Commits

Anonymous committed 376ebdb

Did a lot of overall design thinking, which has resulted in a wide range of small changes and notes in various plugins, their tests and the demos. Started with a rewrite of the state plugin and their tests.

Comments (0)

Files changed (17)

demo/flaggable.html

 		<link rel="stylesheet" type="text/css" href="../lib/jquery-ui-1.8rc1/themes/base/ui.all.css" />
 		<style type="text/css">
 			
-			a.checked {
+			li.checked {
 				background-color: yellow;
 			}
 			
 	
 	<body>
 		<ul>
-			<li>
-				<a href="1">item 1</a>
-				<a href="2">item 2</a>
-			</li>
-			<li>
-				<a href="3">item 3</a>
-				<a href="4">item 4</a>
-				<a href="2">item 2</a>
-				<a href="5">item 5</a>
-			</li>
-				<li>
-					<ul>
-						<li>
-							<a href="6">item 6</a>
-							<a href="2">item 2</a>
-						</li>
-					</ul>
-				</li>
+			<li><input type="checkbox" /> <a href="1">item 1</a></li>
+			<li><input type="checkbox" /> <a href="2">item 2</a></li>
+			<li><input type="checkbox" /> <a href="3">item 3</a></li>
+			<li><input type="checkbox" /> <a href="2">item 2</a></li>
+			<li><input type="checkbox" /> <a href="4">item 4</a></li>
+			<li><input type="checkbox" /> <a href="2">item 2</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.0/jquery-ui.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.flexicallback.js"></script>
 			jQuery(function($) {
 				
 				var $flaggable = $('ul:first'),
-					items = 'a';
+					items = 'li';
 				
-				$flaggable.flaggable({
-					elements: items,
-					handle: function(e) {
+				$flaggable.
+					flaggable({
+						elements: items,
+						data: function() {
+							return parseInt($(this).find('a').attr('href'), 10);
+						},
+						flag: function(e, data) {
+							console.log('flag: ' + data.items + ' - ' + data.unaffected);
+							if (data.items === null) {
+								$flaggable.find(items).
+									addClass('checked').
+									find(':checkbox').attr('checked', true);
+								return;
+							}
+							for (var i = 0; i < data.items.length; i++) {
+								$flaggable.find(items + ' a[href=' + data.items[i] + ']').closest('li').
+									addClass('checked').
+									find(':checkbox').attr('checked', true);
+							}
+						},
+						unflag: function(e, data) {
+							if (data.items === null) {
+								$flaggable.find(items).
+									removeClass('checked').
+									find(':checkbox').attr('checked', false);
+								return;
+							}
+							for (var i = 0; i < data.items.length; i++) {
+								$flaggable.find(items + ' a[href=' + data.items[i] + ']').closest('li').
+									removeClass('checked').
+									find(':checkbox').attr('checked', false);
+							}
+						}
+					}).
+					bind('flaggablechange', function(e, data) {
+						console.log(data.flagged + ' - ' + data.unflagged);
+					}).
+					delegate(items + ' :checkbox', 'change', function(e) {
+						$flaggable.flaggable('flag', parseInt($(this).closest(items).find('a').attr('href'), 10), !$(this).attr('checked'));
+					}).
+					delegate(items + ' a', 'click', function(e) {
 						e.preventDefault();
-					},
-					data: function() {
-						return parseInt($(this).attr('href'));
-					},
-					flag: function(e, data) {
-						if (data.items === null) {
-							$flaggable.find(items).addClass('checked');
-							return;
-						}
-						for (var i = 0; i < data.items.length; i++) {
-							$flaggable.find(items + '[href=' + data.items[i] + ']').addClass('checked');
-						}
-					},
-					unflag: function(e, data) {
-						if (data.items === null) {
-							$flaggable.find(items).removeClass('checked');
-							return;
-						}
-						for (var i = 0; i < data.items.length; i++) {
-							$flaggable.find(items + '[href=' + data.items[i] + ']').removeClass('checked');
-						}
-					}
-				});
+						$flaggable.flaggable('change', parseInt($(this).attr('href'), 10));
+					});
 				
 			});
 			

demo/recipients.html

+<!DOCTYPE html>
+<html lang="en" class="no-js">
+
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+		
+		<title>
+			Recipients
+		</title>
+		
+	    <script type="text/javascript" src="../lib/modernizr-1.5.min.js"></script>
+		<style type="text/css">
+			
+			body > div {
+				display: none;
+			}
+			
+			#sources {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				width: 15%;
+				background-color: grey;
+			}
+			
+			#sources > ul {
+				position: absolute;
+				top: 0;
+				right: 0;
+				bottom: 0;
+				left: 0;
+				overflow-x: hidden;
+				overflow-y: auto;
+			}
+			
+			#sources > ul li.selected {
+				font-weight: bold;
+			}
+			
+			#contacts {
+				position: absolute;
+				top: 0;
+				right: 30%;
+				bottom: 0;
+				left: 15%;
+			}
+			
+			#contacts > ul {
+				position: absolute;
+				top: 2em;
+				right: 0;
+				bottom: 0;
+				left: 0;
+				overflow-x: hidden;
+				overflow-y: auto;
+			}
+			
+			#contacts li.email.checked {
+				background-color: lightgreen;
+			}
+			
+			#contacts li.more {
+				list-style-type: none;
+				color: green;
+			}
+			
+			#contacts > .control {
+				position: absolute;
+				top: 0;
+				right: 0;
+				left: 0;
+				height: 1.4em;
+				background-color: lightgrey;
+				padding: .3em;
+			}
+			
+			#recipients {
+				position: absolute;
+				top: 0;
+				right: 0;
+				bottom: 0;
+				width: 30%;
+				background-color: lightblue;
+			}
+			
+			#recipients > ul {
+				position: absolute;
+				top: 0;
+				right: 0;
+				bottom: 0;
+				left: 0;
+				overflow-x: hidden;
+				overflow-y: auto;
+			}
+			
+		</style>
+	</head>
+	
+	<body>
+		<div id="sources">
+			<ul>
+				<li>All</li>
+				<!--
+					<li class="tag"><%= name %> [<%= count %>]</li>
+				-->
+			</ul>
+		</div>
+		
+		<div id="contacts">
+			<div class="control">
+				<input type="search" />
+				<select>
+					<option value="firstname">First name</option>
+					<option value="lastname">Last name</option>
+					<option value="company">Medium</option>
+				</select>
+				<!--
+					Displaying <%= display %> out of <%= total %>
+				-->
+			</div>
+			<ul>
+				<!--
+					<li class="contact">
+						<div><%= $.trim([first_name, last_name].join(' ')) || '{' + id + '}' %></div>
+						<ul>
+							<!-emails
+								<li class="email"><%= data %></li>
+							->
+						</ul>
+					</li>
+				-->
+				<li class="more"><a href="">load more&hellip;</a></li>
+			</ul>
+		</div>
+		
+		<div id="recipients">
+			<ul>
+				<!--
+					<li class="recipient"><%= name %> &lt;<%= email %>&gt;</li>
+				-->
+			</ul>
+		</div>
+		
+		<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.flexicallback.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.rest.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.employ.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($) {
+				
+				$('#sources').
+					bind('employconstruct', function(e, fcb) {
+						fcb.expect();
+						
+						$.smartpr.tags.get(function(data) {
+							$('#sources ul').dataview('set', data);
+							fcb.call();
+						});
+						
+						$(this).find('ul').
+							flaggable({
+								elements: 'li',
+								// data: true
+								flag: function(e, data) {
+									var $element = $(data.items[0]);
+									$('#sources li').removeClass('selected');
+									$element.addClass('selected');
+									$('#contacts ul').listview('reload');
+								}
+							}).
+							flaggable('change', $(this).find('li')[0]).
+							delegate('li', 'click', function() {
+								$('#sources ul').flaggable('change', this);
+							});
+						
+					});
+				
+				$('#contacts').
+					bind('employconstruct', function(e, fcb) {
+						fcb.expect();
+						
+						$(this).
+							find('ul').
+								listview({
+									data: function(cb, after) {
+										var tag = $($('#sources ul').flaggable('flagged')[0]).dataview('get'),
+											search = $('#contacts .control input[type=search]').val();
+										$.smartpr.contacts.get(function(data, total) {
+											cb(data, total);
+											$('#contacts > .control').dataview('set', {
+												total: $('#contacts ul').listview('totalCount'),
+												display: $('#contacts ul').listview('displayCount')
+											});
+										}, $('#contacts > .control select').val(), $.trim([search, tag ? "tag:" + tag.id : ''].join(' ')), 0);
+										fcb.call();
+									},
+									id: 'id'
+								}).
+								flaggable({
+									elements: 'li.email',
+									change: function(e, data) {
+										$('#contacts li.email').removeClass('checked');
+										for (var i = 0; i < data.flagged.length; i++) {
+											$('#contacts li.email').each(function() {
+												if ($(this).dataview('get') === data.flagged[i].email) {
+													$(this).addClass('checked');
+												}
+											});
+										}
+										$('#recipients ul').listview('reload');
+									},
+									id: 'email'
+								}).
+								delegate('li.email', 'click', function() {
+									$('#contacts ul').flaggable('toggle', {
+										email: $(this).dataview('get'),
+										name: [$(this).closest('li.contact').dataview('get').first_name, $(this).closest('li.contact').dataview('get').last_name].join(' ')
+									});
+								}).
+								find('li.more').bind('click', function(e) {
+									e.preventDefault();
+									$(this).closest('ul').listview('load');
+								}).end().
+								end().
+							find('.control').
+								find('select').bind('change', function() {
+									$('#contacts > ul').listview('reload');
+								}).end().
+								find('input[type=search]').bind('keydown', function() {
+									setTimeout(function() {
+										$('#contacts > ul').listview('reload');
+									}, 300);
+								}).end().
+								end();
+					});
+				
+				$('#recipients').
+					bind('employconstruct', function(e, fcb) {
+						fcb.expect();
+						
+						$(this).find('ul').
+							listview({
+								data: function(cb) {
+									cb($('#contacts ul').flaggable('flagged'));
+									fcb.call();
+								}
+							});
+					});
+				
+				// TODO: Move to one call
+				$('#sources').employ(true);
+				$('#contacts').employ(true);
+				$('#recipients').employ(true);
+				
+			});
+			
+		</script>
+	</body>
+
+</html>

src/jquery.al.data.js

 /*
 WISHLIST
-- Include metadata in fetch.
+- Include metadata plugin in fetch.
 - move to ui widget utility like such:
 	$(elem).tree('get', ns, 'field') , $(elem).tree('set', ns, 'field', 'value')
 	!! PERFORMANCE ???

src/jquery.al.dataview.js

 	};
 };
 
+// TODO: Add 'clear' action
 $.fn.dataview = function(action, templateName, data) {
+	// TODO: What if the data we want to store is a string? This code won't cut it.
 	if (data === undefined && typeof templateName !== 'string') {
 		data = templateName;
 		templateName = undefined;
 					flirt('clear').
 					flirt(data, templateName, function(data) {
 						// transform data to record via recordset (keep an eye on memory!)
-						$(this).store('dataview', 'data', new Record(data));	// rs.get(data)
+						this.store('dataview', 'data', new Record(data));	// rs.get(data)
 					});
 			});
 			break;
 			var record = this.eq(0).parentsUntil('html').andSelf().filter(function() {
 				return !!$(this).fetch('dataview', 'data');
 			}).eq(-1).fetch('dataview', 'data');	// .get()
+			// TODO: If no data found upwards, look downwards in order to support
+			// $('#mydiv').dataview('set', {...1 item...}) and then $('#mydiv').dataview('get')
 			return record instanceof Record ? record.get() : undefined;
 			break;
 		

src/jquery.al.employ.js

 			var $this = $(this);
 			
 			if ($this.is(':hidden')) {
-				$.error("DOM elements cannot be displayed; parent element(s) keep them invisible");
+				$.error("DOM elements cannot be displayed; parent element(s) keep them hidden");
 			} else {
 				fcb.call();
 			}
 // TODO: Rename sleep, ready => hidden, visible (??)
 // TODO: employ(false) may be confusing, as it sounds like: "do not employ" ...
 // 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) {
 	var $this = this,
 		state = status === true ? $.makeArray(arguments).slice(1) : undefined;

src/jquery.al.flaggable.js

 
 */
 
+
+
+
+// TODO: add invalidate method to force/manually signal invalidation
 (function($) {
 
-var dataview = function() {
-	return $(this).dataview('get');
-};
-
 $.widget('al.flaggable', {
 	
 	options: {
 		elements: null,
 		data: false,
-		bind: 'click',
-		handle: $.noop,
-		id: null,
-		flag: $.noop,
-		unflag: $.noop
+		// bind: 'click',
+		// handle: $.noop,
+		id: null
 	},
 	
 	_create: function() {
 		var self = this,
-			flag = self.options.flag,
-			unflag = self.options.unflag;
+			flag = self.options.flag || $.noop,
+			unflag = self.options.unflag || $.noop;
 		
 		if (self.options.data === true) {
-			self.options.data = dataview;	// TODO: Inline function?
+			self.options.data = function() {
+				return $(this).dataview('get');
+			};
 		}
 		
-		$.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);
-			}
-		});
+		// $.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;
 		
-		// If no DOM elements are involved, we are done setting up.
-		if (self.options.elements === null) {
-			return;
-		}
-		
-		self.element.delegate(self.options.elements, self.options.bind, function(e) {
-			self.toggle($.isFunction(self.options.data) ? self.options.data.call(this) : this);
-			return self.options.handle.call(this, e);
-		});
+		// // If no DOM elements are involved, we are done setting up.
+		// if (self.options.elements === null) {
+		// 	return;
+		// }
+		// 
+		// self.element.delegate(self.options.elements, self.options.bind, function(e) {
+		// 	self.toggle($.isFunction(self.options.data) ? self.options.data.call(this) : this);
+		// 	return self.options.handle.call(this, e);
+		// });
 	},
 	
 	_elementsWithData: function(data) {
 		return elements;
 	},
 	
-	flag: function(items, invert) {
+	change: function(flagged, unflagged) {
+		var self = this,
+			items = flagged,
+			invert = false;
+		if (flagged === null) {
+			items = unflagged;
+			invert = true;
+		}
+		return self._change(items, invert);
+	},
+	
+	// TODO: support both (items, invert) and (flagged, unflagged) interfaces:
+	// former in _change, latter in change.
+	// TODO: deal with items=null (and invert!=unflagged)
+	_change: function(items, invert) {
+		var self = this;
 		invert = !!invert;
-		var self = this,
-			current = self._flagged.length,
-			trigger = invert ? 'unflag' : 'flag',
-			modeTrigger = invert ? 'unflagLast' : 'flagFirst';
 		
 		if (items === null) {
-			if (self._inverted !== invert) {
-				return self.flag(self._flagged, invert);
+			items = [];
+			invert = !invert;
+		}
+		items = $.makeArray(items);
+		
+		if (self._inverted === invert) {
+			if (_.isEqual(self._flagged, items) /*self._flagged.equals(items)*/) {
+				return;
 			}
-			self._flagged = [];	// self._flagged.clear();
-			self._inverted = !invert;
-			if (current === 0) {
-				self._trigger(modeTrigger);
-			}
-			self._trigger(trigger, undefined, {items: null});
-			return;
+			// var one = self._flagged.remove(items, true),
+			// 	two = self._flagged.add(items, true),
+			var one = $.merge([], items),
+				two = $.merge([], self._flagged);
+			one.unshift(self._flagged);
+			two.unshift(items);
+			one = _.without.apply(undefined, one);
+			two = _.without.apply(undefined, two);
+			var unflag = [invert ? two : one, null],
+				flag = [invert ? one : two, null];
+		} else {
+			// var one = self._flagged.intersection(items, true),
+			// 	two = self._flagged.union(items, true);
+			var one = _.intersect(self._flagged, items),
+				two = _.uniq($.merge($.merge([], self._flagged), items));
+			var unflag = invert ? [one, null] : [null, two],
+				flag = invert ? [null, two] : [one, null];
+		}
+		var lastFlagged = self._flagged,
+			lastInverted = self._inverted;
+		// self._flagged.set(items);
+		self._flagged = items;
+		self._inverted = invert;
+		if (!$.isArray(unflag[0]) || unflag[0].length > 0) {
+			self._trigger('unflag', undefined, {items: unflag[0], unaffected: unflag[1]});
+		}
+		if (self._inverted === false && self._flagged.length === 0) {
+			self._trigger('unflaglast');
+		}
+		if (!$.isArray(flag[0]) || flag[0].length > 0) {
+			self._trigger('flag', undefined, {items: flag[0], unaffected: flag[1]});
+		}
+		if (lastInverted === false && lastFlagged.length === 0) {
+			self._trigger('flagfirst');
+		}
+		self._trigger('change', undefined, {flagged: invert ? null : self._flagged, unflagged: invert ? self._flagged : null});
+	},
+	
+	flag: function(items, invert) {
+		var self = this;
+		invert = !!invert;
+		
+		if (items === null) {
+			return self._change(items, invert);
 		}
 		
-		if (!$.isArray(items)) {
-			return self.flag([items], invert);
-		}
-		
-		if (items.length === 0) {
-			return;
-		}
+		items = $.makeArray(items);
 		
 		if (self._inverted === invert) {
-			$.merge(self._flagged, items);	// self._flagged.add(items)
-			if (current === 0) {
-				self._trigger(modeTrigger);
-			}
-			self._trigger(trigger, undefined, {items: items});
-			return;
+			return self._change(_.uniq($.merge($.merge([], self._flagged), items)), invert);
 		}
-		
-		// var removed = self._flagged.remove(items);
-		// if (removed.length > 0) {
-		//		self._trigger(trigger, undefined, [$.map(removed, function(record) {
-		//			return record.get();
-		//		})]);
-		// }
-		var impacted = [];
-		for (var i = 0, l = self._flagged.length, p; i < l; i++) {
-			p = $.inArray(items[i], self._flagged);
-			if (p !== -1) {
-				$.merge(impacted, self._flagged.splice(p, 1));
-			}
-		}
-		if (impacted.length > 0) {
-			if (current === impacted.length) {
-				self._trigger(modeTrigger);
-			}
-			self._trigger(trigger, undefined, {items: impacted});
-		}
+		var without = $.merge([], items);
+		without.unshift(self._flagged);
+		without = _.without.apply(undefined, without);
+		return self._change(without, self._inverted);
 	},
 	
 	unflag: function(items) {
 		var self = this;
-		
-		self.flag(items, true);
+		return self.flag(items, true);
 	},
 	
+	// flag: function(items, invert) {
+	// 	invert = !!invert;
+	// 	var self = this,
+	// 		current = self._flagged.length,
+	// 		trigger = invert ? 'unflag' : 'flag',
+	// 		modeTrigger = invert ? 'unflagLast' : 'flagFirst';
+	// 	
+	// 	if (items === null) {
+	// 		if (self._inverted !== invert) {
+	// 			return self.flag(self._flagged, invert);
+	// 		}
+	// 		self._flagged = [];	// self._flagged.clear();
+	// 		self._inverted = !invert;
+	// 		if (current === 0) {
+	// 			self._trigger(modeTrigger);
+	// 		}
+	// 		self._trigger(trigger, undefined, {items: null});
+	// 		return;
+	// 	}
+	// 	
+	// 	if (!$.isArray(items)) {
+	// 		return self.flag([items], invert);
+	// 	}
+	// 	
+	// 	if (items.length === 0) {
+	// 		return;
+	// 	}
+	// 	
+	// 	if (self._inverted === invert) {
+	// 		$.merge(self._flagged, items);	// self._flagged.add(items)
+	// 		if (current === 0) {
+	// 			self._trigger(modeTrigger);
+	// 		}
+	// 		self._trigger(trigger, undefined, {items: items});
+	// 		return;
+	// 	}
+	// 	
+	// 	// var removed = self._flagged.remove(items);
+	// 	// if (removed.length > 0) {
+	// 	//		self._trigger(trigger, undefined, [$.map(removed, function(record) {
+	// 	//			return record.get();
+	// 	//		})]);
+	// 	// }
+	// 	var impacted = [];
+	// 	for (var i = 0, l = self._flagged.length, p; i < l; i++) {
+	// 		p = $.inArray(items[i], self._flagged);
+	// 		if (p !== -1) {
+	// 			$.merge(impacted, self._flagged.splice(p, 1));
+	// 		}
+	// 	}
+	// 	if (impacted.length > 0) {
+	// 		if (current === impacted.length) {
+	// 			self._trigger(modeTrigger);
+	// 		}
+	// 		self._trigger(trigger, undefined, {items: impacted});
+	// 	}
+	// },
+	// 
+	// unflag: function(items) {
+	// 	var self = this;
+	// 	
+	// 	self.flag(items, true);
+	// },
+	
 	toggle: function(items) {
 		var self = this,
 			flag = [],

src/jquery.al.flirt.js

 	// TODO: Do not allow this function to execute if ('this' in data). Throw
 	// exception in that case?
 	return new Function('data',
-		"this.p=[];" +
+		"this.data=data;this.p=[];" +
 		"with(this){" +
 			"this.print=function(){p.push.apply(p,$.map(arguments,esc));};" +
-			"with(data){" +
+			"with((data===undefined||data===null)?this:data){" +
 				"this.p.push('" +
 				template.replace(/[\r\t\n]/g, " ").
 					replace(regexps.singleQuoteHack, "\t").
 // TODO: rename to simply 'esc': consistent with naming inside Flirt, therefore
 // easier to comprehend.
 var escapeHtml = function(token) {
-	var safe = token instanceof Safemarked;
-	if (safe) {
-		token = token.value();
+	// Safemarked tokens should not be touched.
+	if (token instanceof Safemarked) {
+		return token.value();
 	}
-	if (typeof token !== 'object') {
-		token = safe ? ('' + token) : $escaper.text(token).html();
+	// Escaping only applies to string tokens, as a number or a boolean cannot
+	// contain dangerous characters.
+	if (typeof token === 'string') {
+		token = $escaper.text(token).html();
 	}
+	// This function is not in charge of serializing to string values, as we
+	// want to leave that to nodes(), which delegates to Array.prototype.join.
 	return token;
 };
 

src/jquery.al.listview.js

 (function($) {
 
+// TODO: rename 'threshold' option to 'display' ?
 $.widget('al.listview', {
 	
 	options: {
 	_append: function(data) {
 		var self = this;
 		
+		// TODO: Why not accept data instances (as opposed to just arrays of
+		// data instances)?
 		if (!$.isArray(data)) {
 			return;
 		}

src/jquery.al.record.js

 
 [r1, r2, r3, r4] = rs.get()
 
+?? [r1] = rs.set(o1)
+
+?? [r2, r3, r4] = rs.set([o2, o3, o4])
+
 [r4] = rs.remove(o4)
 
-[r1, r2] = rs.remove([o1, o2, o2])
+[r2] = rs.remove([o1, o2, o2])
 
 [] = rs.remove([o1, o5])
 
 [r3] = rs.get()
 
+true = rs.equals(o3)
+
+true = rs.equals([o3, o3])
+
+rs2 = rs.clone()
+
+---
+
+rs.*(items, dry-run)
 
 Record
 ------

src/jquery.al.rest.js

 // of jsonp requested and html retrieved -- not sure about behavior in other
 // scenarios).
 
-$.Rest = function(url, dataType) {
+$.Rest = function(url, dataType, error) {
 	if (!(this instanceof $.Rest)) {
-		return new $.Rest(url, dataType);
+		return new $.Rest(url, dataType, error);
+	}
+	
+	if (!$.isFunction(error)) {
+		error = $.noop;
 	}
 	
 	this.request = function(verb, handler, data, success) {
 				url: url + handler,
 				dataType: dataType,
 				data: data,
+				// contentType: 'application/json',
 				// traditional: true,
 				complete: function(xhr, textStatus) {
 					// console.log('$.ajax complete:');
 					return data;
 				},
 				success: function(data, textStatus, xhr) {
-					// console.log('$.ajax success:');
 					// console.log(data);
 					// console.log(textStatus);
-					success.apply(this, arguments);
+					success.call(this, data);
 				},
-				error: function(xhr, textStatus, error) {
-					console.log('$.ajax error:');
-					console.log(textStatus);
-					console.log(error);
+				error: function(xhr, textStatus, errorThrown) {
+					// console.log('$.ajax error:');
+					// console.log(textStatus);
+					// console.log(error);
+					error.call(this, $.httpData(xhr));
 				}
 			});
 		}, 0);
 	},
 	post: function(handler, data, success) {
 		this.request('POST', handler + '?callback=?', data, success);
+	},
+	put: function(handler, data, success) {
+		this.request('PUT', handler + '?callback=?',  data, success);
 	}
 	
 };

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);
+	
+	// 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;
+		
+		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 (i = 0, l = oldState.length; i < l; i++) {
+				$(oldState[i].route.employ).employ(false);
+			}
+			$this.del('state', 'current');
+		}
+		
+		if (newState) {
+			$this.store('state', 'current', newState);
+			for (i = 0, l = newState.length; i < l; i++) {
+				$(newState[i].route.employ).employ(true);
+			}
+		}
+		
+	});
+	
+};
+
+}(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.)
 
 (function($) {
 
-var ns = 'state',
+var ns = 'state_old',
 	definition = {
 		// Unnamed states are non-final states.
 		name: null,
 		$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:
 		id: 1,
 		name: "Art",
 		fonts: ["Arial", "Verdana"]
-	};
+	},
 	group = {
 		group: "A",
 		members: [
 				fonts: ["Arial", "Verdana"]
 			}
 		]
-	};
-var data = [
+	},
+	data = [
 		group,
 		{
 			group: "B",
 	
 });
 
+// TODO: Test what happens if you employ twice with the same state.
+// TODO: Check that state parameter is ignored when status parameter is false.
+// TODO: Add callback to employ which always calls (even if employ doesn't do anything).
+// TODO: Test synchronicity
+
 }(jQuery));

test/flaggable.js

 (function($) {
 
-var $flaggable;
+var $f, $flaggable;
 
 module('flaggable', {
 	setup: function() {
-		$flaggable = $('#flaggable');
+		$f = $flaggable = $('#flaggable');
 	}
 });
 
-test("Initial state", 2, function() {
+test("flagged & unflagged", 2, function() {
 	
 	$flaggable.flaggable();
 	
 	same($flaggable.flaggable('flagged'), [], "Flagged items is an empty array");
 	equals($flaggable.flaggable('unflagged'), null, "Unflagged items is null, denoting all but flagged");
 	
+	// TODO: flaggable('flagged', <full data list>);
+	
 });
 
-test("Flag and unflag", 28, function() {
+test("change", 8, function() {
 	
-	var count = 0;
-	$flaggable.flaggable({
-		flagFirst: function(e) {
-			count++;
-		},
-		unflagLast: function(e) {
-			count++;
-		}
+	$f.flaggable();
+	
+	$f.one('flaggablechange', function(e, data) {
+		equals(data.flagged, $f.flaggable('flagged'), "Correct set of flagged items is supplied to change handler");
+		equals(data.unflagged, $f.flaggable('unflagged'), "Correct set of unflagged items is supplied to change handler");
 	});
 	
-	$flaggable.bind({
-		flaggableflag: function(e, data) {
+	$f.flaggable('change', 1);
+	same($f.flaggable('flagged'), [1], "Set of flagged items is always a list");
+	equals($f.flaggable('unflagged'), null, "Set of unflagged items is null");
+	
+	$f.flaggable('change', [2, 3]);
+	same($f.flaggable('flagged'), [2, 3], "Set of flagged items is changed entirely");
+	
+	$f.
+		one('flaggableflag', function(e) {
+			ok(true, "flag event is triggered upon change");
+		}).
+		one('flaggableunflag', function(e, data) {
+			ok(false, "This point should not be reached, as no unflag event must be triggered (with items: " + data.items + ")");
+		});
+	
+	$f.flaggable('change', null, [1]);
+	equals($f.flaggable('flagged'), null, "Set of flagged items is null");
+	same($f.flaggable('unflagged'), [1], "Set of unflagged items is a list");
+	
+});
+
+test("flag & unflag", 28, function() {
+	
+	$flaggable.
+		one('flaggableflag', function(e, data) {
 			equals(data.items, null, "flag event triggered with correct items");
-		},
-		flaggableunflag: function(e, data) {
+		}).
+		one('flaggableunflag', function(e, data) {
 			ok(false, "This point should not be reached, as no unflag event must be triggered (with items: " + data.items + ")");
-		}
-	});
+		}).
+		one('flaggablechange', function(e, data) {
+			ok(true, "One change event is triggered for every atomic change");
+		}).
+		flaggable();
+	
 	$flaggable.flaggable('flag', null);
 	equals($flaggable.flaggable('flagged'), null, "Flagging all items after all items were implicitly non-flagged does impact flagged items");
 	same($flaggable.flaggable('unflagged'), [], "And it also has an effect on unflagged items");
-	$flaggable.unbind();
 	
 	$flaggable.bind({
 		flaggableflag: function(e, data) {
 			same(data.items, [1, 3], "unflag event triggered with correct items");
 		}
 	});
-	$flaggable.flaggable('toggle', [1, 3, 4]);
+	$flaggable.flaggable('change', [2, 4]);		// TODO: Use flag/unflag here.
 	same($flaggable.flaggable('flagged'), [2, 4], "Unflagging explicitly flagged items or implicitly non-flagged items does impact flagged items");
 	equals($flaggable.flaggable('unflagged'), null, "But it does not have an effect on unflagged items");
 	$flaggable.unbind();
 			ok(false, "This point should not be reached, as no flag event must be triggered (with items: " + data.items + ")");
 		},
 		flaggableunflag: function(e, data) {
-			same(data.items, [2], "unflag event triggered with correct items");
+			same(data.items, [2, 4], "unflag event triggered with correct items");
 		}
 	});
 	$flaggable.flaggable('unflag', null);
-	same($flaggable.flaggable('flagged'), [4], "Unflagging all items after some items were explicitly flagged does impact flagged items");
+	same($flaggable.flaggable('flagged'), [], "Unflagging all items after some items were explicitly flagged does impact flagged items");
 	equals($flaggable.flaggable('unflagged'), null, "But it does not have an effect on unflagged items");
 	$flaggable.unbind();
 	
-	$flaggable.flaggable('toggle', 4);
-	equals(count, 4, "flagFirst and unflagLast callbacks are being called correctly");
 });
 
-test("Link to DOM; elements are flagged", 2, function() {
+test("toggle", 2, function() {
 	
-	var items = 'a',
-		$items = $flaggable.find(items),
-		count = 0;
+	$f.flaggable();
 	
-	$flaggable.flaggable({
-		elements: items,
-		handle: function(e) {
+	$f.flaggable('flag', 1);
+	$f.flaggable('toggle', [1, 2, 3]);
+	same($f.flaggable('flagged'), [2, 3], "Toggle explicitly flagged items");
+	
+	$f.flaggable('change', null, [1, 2]);
+	$f.flaggable('toggle', [2, 3]);
+	same($f.flaggable('unflagged'), [1, 3], "Toggle implicitly flagged items");
+	
+});
+
+// TODO: Test taking in DOM elements that map to data automagically
+// (change, flag, unflag, toggle)
+
+test("flagfirst & unflaglast", 2, function() {
+	
+	$f.
+		bind('flaggableflagfirst flaggableunflaglast', function(e) {
+			ok(true, e.type + " event triggered");
+		}).
+		flaggable();
+	
+	$f.flaggable('flag', 4);
+	$f.flaggable('unflag', null);
+	
+});
+
+test("Link to DOM; elements are flaggable items", 2, function() {
+	
+	var $items = $f.find('a');
+	
+	$f.
+		one('flaggableflag', function(e, data) {
+			same(data.items, [$items[0]], "Clicked element is passed to flag handler");
+		}).
+		flaggable({
+			elements: 'a'
+		}).
+		delegate('a', 'click', function(e) {
 			e.preventDefault();
-		},
-		flag: function(e, data) {
-			if (count === 0) {
-				same(data.items, [$items[0]], "Clicked element is passed to flag handler");
-			}
-			count++;
-		}
-	});
+			$f.flaggable('toggle', this);
+		});
 	
 	$items.eq(0).click();
 	$items.eq(2).click();
-	same($flaggable.flaggable('flagged'), [$items[0], $items[2]], "Clicked elements are flagged items");
+	same($f.flaggable('flagged'), [$items[0], $items[2]], "Clicked elements are flagged items");
 	
 });
 
-test("Link to DOM; data that is retrieved from elements are flagged", 5, function() {
+test("Link to DOM; data that are retrieved from elements are flaggable items", 5, function() {
+	
+	var $items = $f.find('a'),
+		count = 0;
+	
+	$f.
+		one('flaggableflag', function(e, data) {
+			same(data.items, [1], "Clicked element's data are passed to flag handler");
+		}).
+		flaggable({
+			elements: 'a',
+			data: function() {
+				return parseInt($(this).attr('href'), 10);
+			},
+			invalidateFlagged: function(e, data) {
+				if (count === 1) {
+					same(data.elements, [$items[1], $items[4], $items[7]], "Elements corresponding to data from flagged element are passed to invalidateFlagged handler");
+				}
+				if (count === 2) {
+					same(data.elements, $items.get(), "All elements are passed to invalidateFlagged handler when all data is flagged");
+				}
+				count++;
+			},
+			invalidateUnflagged: function(e, data) {
+				same(data.elements, [$items[0]], "Element corresponding to data from unflagged element is passed to invalidateUnflagged handler");
+			}
+		}).
+		delegate('a', 'click', function(e) {
+			e.preventDefault();
+			$f.flaggable('flag', this);
+		});
 
-	var items = 'a',
-		$items = $flaggable.find(items),
-		count1 = 0, count2 = 0;
-	
-	$flaggable.flaggable({
-		elements: items,
-		handle: function(e) {
-			e.preventDefault();
-		},
-		data: function() {
-			return parseInt($(this).attr('href'));
-		},
-		flag: function(e, data) {
-			if (count1++ === 0) {
-				same(data.items, [1], "Clicked element's data is passed to flag handler");
-			}
-		},
-		invalidateFlagged: function(e, data) {
-			if (count2 === 1) {
-				same(data.elements, [$items[1], $items[4], $items[7]], "Elements corresponding to data from flagged element are passed to invalidateFlagged handler");
-			}
-			if (count2 === 2) {
-				same(data.elements, $items.get(), "All elements are passed to invalidateFlagged handler when all data is flagged");
-			}
-			count2++;
-		},
-		invalidateUnflagged: function(e, data) {
-			same(data.elements, [$items[0]], "Element corresponding to data from unflagged element is passed to invalidateUnflagged handler");
-		}
-	});
+	$items.eq(0).click();
+	$items.eq(1).click();
+	same($f.flaggable('flagged'), [1, 2], "Clicked elements' data are flagged items");
 	
 	$items.eq(0).click();
-	$items.eq(1).click();
-	same($flaggable.flaggable('flagged'), [1, 2], "Clicked elements' data are flagged items");
-	
-	$items.eq(0).click();
-	$flaggable.flaggable('flag', null);
+	$f.flaggable('flag', null);
 	
 });
 
 				name: "<b>Amsterdam</b>",
 				fonts: ["Arial", "Verdana"]
 			}
-		]
+		],
+		data: "not a data object"
 	},
 	data = [
 		group,
 				}
 			]
 		}
+	],
+	messy = [
+		{
+			group: 5,
+			members: [
+				{
+					id: undefined,
+					name: "Ch>r>cters that should b& <scap<d and Unicode: áœØ™‹¢ÅÛ—±≥÷√∂",
+					fonts: [undefined, "arial", null, true, 0]
+				}
+			]
+		}
 	];
-testdata = data;
+
 module('flirt', {
 	setup: function() {
 		$flirt = $('#flirt');
 	
 });
 
-test('$.flirt as a template parser', 5, function() {
+test("$.flirt as template parser: environment", 7, function() {
+	
+	var parsed = $('<div />').append($.flirt("\
+		<%= this.data.id %>,\
+		<%= data %>,\
+		<%= 'flirt' in this %>,\
+		<%= 'callback' in this %>,\
+		<%= 'safe' in this %>,\
+		<%= 'esc' in this %>,\
+		<%= 'nodes' in this %>\
+	", [{id: 1, data: 'data'}])).text().split(',');
+	
+	equals($.trim(parsed[0]), "1", "this.data is the full data object");
+	equals($.trim(parsed[1]), "data", "data is the value of the data field, if it exists");
+	equals($.trim(parsed[2]), "true", "this.flirt");
+	equals($.trim(parsed[3]), "true", "this.callback");
+	equals($.trim(parsed[4]), "true", "this.safe");
+	equals($.trim(parsed[5]), "true", "this.esc");
+	equals($.trim(parsed[6]), "true", "this.nodes");
+	
+});
+
+test("$.flirt as template parser: errors", 1, function() {
+	
+	try {
+		$.flirt("<%= doesnotexist %>", [1]);
+	} catch (err) {
+		ok(err, "Error is thrown if non-existent data field is referenced");
+	}
+	
+});
+
+test('$.flirt as template parser', 5, function() {
 	
 	var count = 0;
 	
+	// TODO: $parsed global var(??)
 	$parsed = $('<div />').append($.flirt(template, data, function(d) {
 		if (d === member && count++ === 0) {
 			ok(true, "Callback at member level");
 //			equals($parsed.find('strong').length, 2, "Callback at group level: parsed data and template contains correct amount of 'strong' elements");
 		}
 	}));
+	
 	equals(flatten($parsed.text()), 'a:[artarial,verdana,][<b>amsterdam</b>arial,verdana,](2)thosewereids1,2,...b:[businessarial,verdana,](1)thosewereids3,...c:[co<mpute>rsdoes-not-exist,\'timesnewroman\',][cool\'couriernew\',courier,](2)thosewereids4,5,...', "Compile and parse template: parsed data and template matches textwise, HTML is escaped");
 	equals($parsed.children('li').eq(4).find('span.safe').length, 1, "Data that has been marked as safe is not escaped");
 	equals($parsed.children('li').length, 6, "Compile and parse template: correct amount of 'li' elements");
 	
 });
 
+test("$.flirt as template parser: messy data", 2, function() {
+	
+	var result = $('<div />').append($.flirt(template, messy)).text();
+	
+	ok(result.indexOf(messy[0].members[0].name) !== -1, "Strings containing characters that should be encoded are represented correctly");
+	ok(flatten(result).indexOf(messy[0].members[0].fonts.join(',')) !== -1, "Values are serialized to string according to Array.prototype.join's behavior");
+	
+});
+
 test('$.fn.flirt', 5, function() {
 	
 	$flirt.flirt(data);
 	    <script type="text/javascript" src="../lib/modernizr-1.5.min.js"></script>
 		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
 		<script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>
-		<script type="text/javascript">
-			// Prevents escaping of object descriptions. Wonder why QUnit
-			// doesn't disable this by default.
-			QUnit.jsDump.HTML = false;
-		</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="../lib/jquery.ba-hashchange.js"></script>
 jQuery(function($) {
 
-var $window = $(window),
-	$blocks = $('#block0, #block1, #block2, #block3'),
+module('state', {
+	setup: function() {
+		$('#main').show();
+		$.state(
+			{
+				pattern: /^\//,
+				employ: '#block0'
+			},
+			{
+				name: 'static',
+				pattern: /^\/static$/,
+				employ: '#block1, #block2'
+			},
+			{
+				name: 'witharg',
+				pattern: /^\/witharg\/(\w+)$/,
+				employ: '#block2, #block3'
+			}
+		);
+	},
+	teardown: function() {
+		// TODO: Replace with $.state('destroy') call.
+		$(window).
+			unbind('hashchange').
+			del('state', 'current');
+		location.hash = '';
+		$('#main').hide();
+	}
+});
+
+asyncTest('Change from unmatched state to state without parameters', 3, function() {
+	var count = 0;
 	
-	wait = function(callback) {
-		if (callback === undefined) {
-			callback = start;
-		}
-		setTimeout(callback, $.hashchangeDelay);
-	},
-	onenter = $.noop,
-	onleave = $.noop;
-
-$.state(
-	{
-		pattern: '^\/',
-		elements: $blocks.eq(0)
-	},
-	{
-		name: 'static',
-		pattern: '^\/static$',
-		elements: '#block1, #block2',
-		enter: function() {
-			onenter.apply(this, arguments);
-		},
-		leave: function() {
-			onleave.apply(this, arguments);
-		}
-	},
-	{
-		name: 'witharg',
-		pattern: /^\/witharg\/(\w+)$/,
-		elements: [$('#block2')[0], $('#block3')[0]],
-		enter: function() {
-			onenter.apply(this, arguments);
-		},
-		leave: function() {
-			onleave.apply(this, arguments);
-		}
-	}
-);
-
-module('state');
-
-asyncTest('Change to state without parameters', 11 /*13*/, function() {
-	onenter = function() {
-		ok('name' in this && 'pattern' in this && 'elements' in this, "Controller is being called on state enter with 'this' being the state definition");
-		equals(this.name, 'static', "And with the correct state definition");
-	};
-	
-	$window.bind({
-		'stateenter._': function(e, name) {
-			equals(e.type, 'stateenter', "General global stateenter event is triggered");
-			equals(name, 'static', "And with the correct state definition");
-			same(e.states[e.states.length - 1].params, [], "No parameters is represented as an empty array");
-			equals(this, window, "While 'this' is (as usual) the element to which the handler is bound");
-		},
-		'stateenter.static': function(e) {
-			equals(e.type, 'stateenter', "Matching state-specific global stateenter event is triggered");
-		},
-		'stateenter.witharg': function(e) {
-			ok(false, "Non-matching state-specific global stateenter event is not triggered");
+	$('#block0, #block1, #block2, #block3').bind('employsleep', function() {
+		ok(false, "#" + this.id + " should not be unemployed");
+	});
+	$('#block0, #block1, #block2').bind('employconstruct', function() {
+		switch (count++) {
+			case 0:
+				equals(this.id, 'block0', "#block0 is employed first");
+				break;
+			case 1:
+				equals(this.id, 'block1', "#block1 is employed second");
+				break;
+			case 2:
+				equals(this.id, 'block2', "#block2 is employed third");
+				break;
 		}
 	});
-	
-	$blocks.slice(0, 3).bind({
-		'stateenter._': function(e, name) {
-			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
-			equals(name, 'static', "And with the correct state definition");
-		}
-	});
-	$blocks.eq(3).bind({
-		'stateenter._': function(e) {
-			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
-		}
+	$('#block4').bind('employconstruct', function() {
+		ok(false, "#" + this.id + " should not be employed");
 	});
 	
-	window.location.hash = '/static';
-	
-	wait(function() {
-		$window.unbind('stateenter stateleave');
-		$blocks.unbind('stateenter stateleave');
+	$('#block2').bind('employconstruct', function() {
 		start();
 	});
+	
+	location.hash = '/static';
+	
 });
 
-asyncTest('Change to state with parameters', 8 /*9*/, function() {
-	onenter = function() {
-		same($.makeArray(arguments), ['hello'], "If the entered state defines parameters, their value(s) are passed to the controller");
-	};
-	onleave = function() {
-		ok('name' in this && 'pattern' in this && 'elements' in this, "Controller is being called on state leave with 'this' being the state definition");
-		equals(this.name, 'static', "And with the correct state definition");
-	};
+asyncTest('Change from state without parameters to unmatched state', 3, function() {
+	var count = 0;
 	
-	$window.bind({
-		'stateenter.witharg': function(e) {
-			var args = $.makeArray(arguments).slice(2);
-			same(args, ['hello'], "If the entered state defines parameters, their value(s) are passed as arguments to the event handler");
-			same(args, e.states[e.states.length - 1].params, "And also in the event object");
-		},
+	location.hash = '/static';
+	
+	$(window).bind('hashchange', function() {
+		
+		$('#block0, #block1, #block2').bind('employsleep', function() {
+			switch (count++) {
+				case 0:
+					equals(this.id, 'block0', "#block0 is unemployed first");
+					break;
+				case 1:
+					equals(this.id, 'block1', "#block1 is unemployed second");
+					break;
+				case 2:
+					equals(this.id, 'block2', "#block2 is unemployed third");
+					break;
+			}
+		});
+		$('#block4').bind('employsleep', function() {
+			ok(false, "#" + this.id + " should not be unemployed");
+		});
+		$('#block0, #block1, #block2, #block3').bind('employready', function() {
+			ok(false, "#" + this.id + " should not be employed");
+		});
+	
+		$('#block2').bind('employsleep', function() {
+			start();
+		});
+	
+		// This only matches a non-final state, so all blocks should be
+		// unemployed as a result.
+		location.hash = '/';
+		
 	});
 	
-	$([$blocks[0], $blocks[2], $blocks[3]]).bind({
-		'stateenter._': function(e, name) {
-			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
-			equals(name, 'witharg', "And with the correct state definition");
-		}
-	});
-	$blocks.eq(1).bind({
-		'stateenter._': function(e) {
-			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
-		}
-	});
-	
-	window.location.hash = '/witharg/hello';
-	
-	wait(function() {
-		$window.unbind('stateenter stateleave');
-		$blocks.unbind('stateenter stateleave');
-		start();
-	});
 });
 
-asyncTest('Change to same state with different parameters', 4, function() {
-	$([$blocks[0], $blocks[2]]).bind({
-		'stateenter._': function(e, name) {
-			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
-			equals(name, 'static', "And with the correct state definition");
-		}
-	});
-	$([$blocks[1], $blocks[3]]).bind({
-		'stateenter._': function(e) {
-			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
-		}
-	});
-	
-	window.location.hash = '/static';
-	
-	wait(function() {
-		$window.unbind('stateenter stateleave');
-		$blocks.unbind('stateenter stateleave');
-		start();
-	});
+// TODO: Write test for moving to the same state (due to hash being changed
+// twice right after each other, resulting in the event handler finding the
+// same value in location.hash). --> should be solved automatically as soon as
+// we calculate intersections between states.
+
 });
 
-asyncTest('Change back to previously visited state', 6, function() {
-	$([$blocks[0], $blocks[2], $blocks[3]]).bind({
-		'stateenter._': function(e, name) {
-			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
-			equals(name, 'witharg', "And with the correct state definition");
-		}
-	});
-	$blocks.eq(1).bind({
-		'stateenter._': function(e) {
-			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
-		}
-	});
-	
-	window.location.hash = '/witharg/goodbye';
-	
-	wait(function() {
-		$window.unbind('stateenter stateleave');
-		$blocks.unbind('stateenter stateleave');
-		start();
-	});
-});
-
-asyncTest('Reset state', 6 /*7*/, function() {
-	onleave = function() {
-		same($.makeArray(arguments), ['goodbye'], "If the left state defines parameters, their value(s) are passed to the controller");
-	};
-	onenter = function() {
-		ok(false, "This point should not be reached, as no state matches and thus no controller is invoked");
-	};
-	$window.bind({
-		'stateleave._': function(e, name) {
-			equals(e.type, 'stateleave', "General global stateleave event is triggered");
-			equals(name, 'witharg', "And with the correct state definition");
-			var args = $.makeArray(arguments).slice(2);
-			same(args, ['goodbye'], "If the left state defines parameters, their value(s) are passed as arguments to the event handler");
-			same(args, e.states[e.states.length - 1].params, "And also in the event object");
-		},
-		'stateleave.witharg': function(e) {
-			equals(e.type, 'stateleave', "Matching state-specific global stateleave event is triggered");
-		},
-		'stateleave.static': function(e) {
-			ok(false, "Non-matching state-specific global stateleave event is not triggered");
-		}
-	});
-	window.location.hash = '#';
-	
-	wait(function() {
-		// TODO: Use $.fetch() as soon as we have implemented it.
-		equals($window.fetch('state', 'current'), undefined, "No state matches so no current state should be held");
-		$window.unbind('stateenter stateleave');
-		$blocks.unbind('stateenter stateleave');
-		start()
-	});
-});
-
-});
+// jQuery(function($) {
+// 
+// var $window = $(window),
+// 	$blocks = $('#block0, #block1, #block2, #block3'),
+// 	
+// 	wait = function(callback) {
+// 		if (callback === undefined) {
+// 			callback = start;
+// 		}
+// 		setTimeout(callback, $.hashchangeDelay);
+// 	},
+// 	onenter = $.noop,
+// 	onleave = $.noop;
+// 
+// $.state(
+// 	{
+// 		pattern: '^\/',
+// 		elements: $blocks.eq(0)
+// 	},
+// 	{
+// 		name: 'static',
+// 		pattern: '^\/static$',
+// 		elements: '#block1, #block2',
+// 		enter: function() {
+// 			onenter.apply(this, arguments);
+// 		},
+// 		leave: function() {
+// 			onleave.apply(this, arguments);
+// 		}
+// 	},
+// 	{
+// 		name: 'witharg',
+// 		pattern: /^\/witharg\/(\w+)$/,
+// 		elements: [$('#block2')[0], $('#block3')[0]],
+// 		enter: function() {
+// 			onenter.apply(this, arguments);
+// 		},
+// 		leave: function() {
+// 			onleave.apply(this, arguments);
+// 		}
+// 	}
+// );
+// 
+// module('state');
+// 
+// asyncTest('Change to state without parameters', 11 /*13*/, function() {
+// 	onenter = function() {
+// 		ok('name' in this && 'pattern' in this && 'elements' in this, "Controller is being called on state enter with 'this' being the state definition");
+// 		equals(this.name, 'static', "And with the correct state definition");
+// 	};
+// 	
+// 	$window.bind({
+// 		'stateenter._': function(e, name) {
+// 			equals(e.type, 'stateenter', "General global stateenter event is triggered");
+// 			equals(name, 'static', "And with the correct state definition");
+// 			same(e.states[e.states.length - 1].params, [], "No parameters is represented as an empty array");
+// 			equals(this, window, "While 'this' is (as usual) the element to which the handler is bound");
+// 		},
+// 		'stateenter.static': function(e) {
+// 			equals(e.type, 'stateenter', "Matching state-specific global stateenter event is triggered");
+// 		},
+// 		'stateenter.witharg': function(e) {
+// 			ok(false, "Non-matching state-specific global stateenter event is not triggered");
+// 		}
+// 	});
+// 	
+// 	$blocks.slice(0, 3).bind({
+// 		'stateenter._': function(e, name) {
+// 			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
+// 			equals(name, 'static', "And with the correct state definition");
+// 		}
+// 	});
+// 	$blocks.eq(3).bind({
+// 		'stateenter._': function(e) {
+// 			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
+// 		}
+// 	});
+// 	
+// 	window.location.hash = '/static';
+// 	
+// 	wait(function() {
+// 		$window.unbind('stateenter stateleave');
+// 		$blocks.unbind('stateenter stateleave');
+// 		start();
+// 	});
+// });
+// 
+// asyncTest('Change to state with parameters', 8 /*9*/, function() {
+// 	onenter = function() {
+// 		same($.makeArray(arguments), ['hello'], "If the entered state defines parameters, their value(s) are passed to the controller");
+// 	};
+// 	onleave = function() {
+// 		ok('name' in this && 'pattern' in this && 'elements' in this, "Controller is being called on state leave with 'this' being the state definition");
+// 		equals(this.name, 'static', "And with the correct state definition");
+// 	};
+// 	
+// 	$window.bind({
+// 		'stateenter.witharg': function(e) {
+// 			var args = $.makeArray(arguments).slice(2);
+// 			same(args, ['hello'], "If the entered state defines parameters, their value(s) are passed as arguments to the event handler");
+// 			same(args, e.states[e.states.length - 1].params, "And also in the event object");
+// 		},
+// 	});
+// 	
+// 	$([$blocks[0], $blocks[2], $blocks[3]]).bind({
+// 		'stateenter._': function(e, name) {
+// 			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
+// 			equals(name, 'witharg', "And with the correct state definition");
+// 		}
+// 	});
+// 	$blocks.eq(1).bind({
+// 		'stateenter._': function(e) {
+// 			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
+// 		}
+// 	});
+// 	
+// 	window.location.hash = '/witharg/hello';
+// 	
+// 	wait(function() {
+// 		$window.unbind('stateenter stateleave');
+// 		$blocks.unbind('stateenter stateleave');
+// 		start();
+// 	});
+// });
+// 
+// asyncTest('Change to same state with different parameters', 4, function() {
+// 	$([$blocks[0], $blocks[2]]).bind({
+// 		'stateenter._': function(e, name) {
+// 			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
+// 			equals(name, 'static', "And with the correct state definition");
+// 		}
+// 	});
+// 	$([$blocks[1], $blocks[3]]).bind({
+// 		'stateenter._': function(e) {
+// 			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
+// 		}
+// 	});
+// 	
+// 	window.location.hash = '/static';
+// 	
+// 	wait(function() {
+// 		$window.unbind('stateenter stateleave');
+// 		$blocks.unbind('stateenter stateleave');
+// 		start();
+// 	});
+// });
+// 
+// asyncTest('Change back to previously visited state', 6, function() {
+// 	$([$blocks[0], $blocks[2], $blocks[3]]).bind({
+// 		'stateenter._': function(e, name) {
+// 			equals(e.type, 'stateenter', "General element-level stateenter event is triggered on element #" + this.id);
+// 			equals(name, 'witharg', "And with the correct state definition");
+// 		}
+// 	});
+// 	$blocks.eq(1).bind({
+// 		'stateenter._': function(e) {
+// 			ok(false, "This point should not be reached, as stateenter event should not be triggered on element #" + this.id);
+// 		}
+// 	});
+// 	
+// 	window.location.hash = '/witharg/goodbye';
+// 	
+// 	wait(function() {
+// 		$window.unbind('stateenter stateleave');
+// 		$blocks.unbind('stateenter stateleave');
+// 		start();
+// 	});
+// });
+// 
+// asyncTest('Reset state', 6 /*7*/, function() {
+// 	onleave = function() {
+// 		same($.makeArray(arguments), ['goodbye'], "If the left state defines parameters, their value(s) are passed to the controller");
+// 	};
+// 	onenter = function() {
+// 		ok(false, "This point should not be reached, as no state matches and thus no controller is invoked");
+// 	};
+// 	$window.bind({
+// 		'stateleave._': function(e, name) {
+// 			equals(e.type, 'stateleave', "General global stateleave event is triggered");
+// 			equals(name, 'witharg', "And with the correct state definition");
+// 			var args = $.makeArray(arguments).slice(2);
+// 			same(args, ['goodbye'], "If the left state defines parameters, their value(s) are passed as arguments to the event handler");
+// 			same(args, e.states[e.states.length - 1].params, "And also in the event object");
+// 		},
+// 		'stateleave.witharg': function(e) {
+// 			equals(e.type, 'stateleave', "Matching state-specific global stateleave event is triggered");
+// 		},
+// 		'stateleave.static': function(e) {
+// 			ok(false, "Non-matching state-specific global stateleave event is not triggered");
+// 		}
+// 	});
+// 	window.location.hash = '#';
+// 	
+// 	wait(function() {
+// 		// TODO: Use $.fetch() as soon as we have implemented it.
+// 		equals($window.fetch('state', 'current'), undefined, "No state matches so no current state should be held");
+// 		$window.unbind('stateenter stateleave');
+// 		$blocks.unbind('stateenter stateleave');
+// 		start()
+// 	});
+// });
+// 
+// });