From 499013f4fb4537a2472826e8c6f299603766f58a Mon Sep 17 00:00:00 2001
From: Lars Jung <lrsjng@gmail.com>
Date: Thu, 18 Oct 2012 21:27:09 +0200
Subject: [PATCH] Initial switch to smart browsing.

---
 src/_h5ai/client/js/inc/core/entry.js         |  12 +-
 src/_h5ai/client/js/inc/core/location.js      | 117 +++++++++++++++---
 src/_h5ai/client/js/inc/core/refresh.js       |   4 +-
 src/_h5ai/client/js/inc/core/server.js        |   4 +-
 src/_h5ai/client/js/inc/ext/crumb.js          |  54 +++++---
 src/_h5ai/client/js/inc/ext/custom.js         |  34 +++--
 .../client/js/inc/ext/link-hover-states.js    |  19 ++-
 src/_h5ai/client/js/inc/main.js               |   4 +-
 src/_h5ai/client/js/inc/model/entry.js        |  24 +++-
 .../js/inc/view/{extended.js => items.js}     |  61 +++++----
 src/_h5ai/conf/options.json                   |   5 +-
 src/_h5ai/server/php/index.php.jade           |   3 -
 12 files changed, 244 insertions(+), 97 deletions(-)
 rename src/_h5ai/client/js/inc/view/{extended.js => items.js} (75%)

diff --git a/src/_h5ai/client/js/inc/core/entry.js b/src/_h5ai/client/js/inc/core/entry.js
index ace3e99f..03e374a4 100644
--- a/src/_h5ai/client/js/inc/core/entry.js
+++ b/src/_h5ai/client/js/inc/core/entry.js
@@ -1,13 +1,5 @@
 
-modulejs.define('core/entry', ['_', 'config', 'model/entry'], function (_, config, Entry) {
+modulejs.define('core/entry', ['core/location'], function (location) {
 
-	_.each(config.entries || [], function (entry) {
-
-		Entry.get(entry.absHref, entry.time, entry.size, entry.status, entry.content);
-	});
-
-	var entry = Entry.get();
-	entry.status = '=h5ai=';
-
-	return entry;
+	return location.getItem();
 });
diff --git a/src/_h5ai/client/js/inc/core/location.js b/src/_h5ai/client/js/inc/core/location.js
index bea9577b..8a870525 100644
--- a/src/_h5ai/client/js/inc/core/location.js
+++ b/src/_h5ai/client/js/inc/core/location.js
@@ -1,7 +1,13 @@
 
-modulejs.define('core/location', [], function () {
+modulejs.define('core/location', ['_', 'modernizr', 'core/settings', 'core/event', 'core/notify'], function (_, modernizr, allsettings, event, notify) {
 
-	var doc = document,
+	var settings = _.extend({
+			smartBrowsing: false
+		}, allsettings.view),
+
+		doc = document,
+
+		history = settings.smartBrowsing && modernizr.history ? window.history : null,
 
 		forceEncoding = function (href) {
 
@@ -17,36 +23,109 @@ modulejs.define('core/location', [], function () {
 					.replace(/\=/g, '%3D');
 		},
 
-		absHref = (function () {
+		reUriToPathname = /^.*:\/\/[^\/]*|[^\/]*$/g,
+		uriToPathname = function (uri) {
 
-			var rePrePathname = /.*:\/\/[^\/]*/,
-				rePostPathname = /[^\/]*$/,
+			return uri.replace(reUriToPathname, '');
+		},
 
-				uriToPathname = function (uri) {
+		hrefsAreDecoded = (function () {
 
-					return uri.replace(rePrePathname, '').replace(rePostPathname, '');
-				},
-
-				testpathname = '/a b',
-				a = doc.createElement('a'),
-				isDecoded, location;
+			var testpathname = '/a b',
+				a = doc.createElement('a');
 
 			a.href = testpathname;
-			isDecoded = uriToPathname(a.href) === testpathname;
+			return uriToPathname(a.href) === testpathname;
+		}()),
 
-			a.href = doc.location.href;
+		encodedHref = function (href) {
+
+			var a = doc.createElement('a'),
+				location;
+
+			a.href = href;
 			location = uriToPathname(a.href);
 
-			if (isDecoded) {
+			if (hrefsAreDecoded) {
 				location = encodeURIComponent(location).replace(/%2F/ig, '/');
 			}
 
 			return forceEncoding(location);
-		}());
+		};
+
+
+	var absHref = null,
+
+		getDomain = function () {
+
+			return doc.domain;
+		},
+
+		getAbsHref = function () {
+
+			return absHref;
+		},
+
+		getItem = function () {
+
+			return modulejs.require('model/entry').get(absHref);
+		},
+
+		setLocation = function (newAbsHref, keepBrowserUrl) {
+
+			newAbsHref = encodedHref(newAbsHref);
+			if (absHref !== newAbsHref) {
+				absHref = newAbsHref;
+				event.pub('location.changed', absHref);
+
+				notify.set('loading...');
+				modulejs.require('core/refresh')(function () { notify.set(); });
+
+				if (history) {
+					if (keepBrowserUrl) {
+						history.replaceState({absHref: absHref}, '', absHref);
+					} else {
+						history.pushState({absHref: absHref}, '', absHref);
+					}
+				}
+			}
+		},
+
+		setLink = function ($el, item) {
+
+			$el.attr('href', item.absHref);
+
+			if (history && item.isFolder() && item.status === '=h5ai=') {
+				$el.on('click', function () {
+
+					setLocation(item.absHref);
+					return false;
+				});
+			}
+
+			if (item.status !== '=h5ai=') {
+				$el.attr('target', '_blank');
+			}
+		};
+
+
+	if (history) {
+		window.onpopstate = function (e) {
+
+			if (e.state && e.state.absHref) {
+				setLocation(e.state.absHref, true);
+			}
+		};
+	}
+
 
 	return {
-		domain: doc.domain,
-		absHref: absHref,
-		forceEncoding: forceEncoding
+		forceEncoding: forceEncoding,
+		encodedHref: encodedHref,
+		getDomain: getDomain,
+		getAbsHref: getAbsHref,
+		getItem: getItem,
+		setLocation: setLocation,
+		setLink: setLink
 	};
 });
diff --git a/src/_h5ai/client/js/inc/core/refresh.js b/src/_h5ai/client/js/inc/core/refresh.js
index 23d60b84..0ac66bcf 100644
--- a/src/_h5ai/client/js/inc/core/refresh.js
+++ b/src/_h5ai/client/js/inc/core/refresh.js
@@ -1,5 +1,5 @@
 
-modulejs.define('core/refresh', ['_', 'core/server', 'model/entry'], function (_, server, Entry) {
+modulejs.define('core/refresh', ['_', 'config', 'core/server', 'model/entry', 'core/location'], function (_, config, server, Entry, location) {
 
 	var parseJson = function (entry, json) {
 
@@ -20,7 +20,7 @@ modulejs.define('core/refresh', ['_', 'core/server', 'model/entry'], function (_
 
 		refresh = function (callback) {
 
-			var entry = Entry.get();
+			var entry = Entry.get(location.getAbsHref());
 
 			server.request({action: 'get', entries: true, entriesHref: entry.absHref, entriesWhat: 1}, function (json) {
 
diff --git a/src/_h5ai/client/js/inc/core/server.js b/src/_h5ai/client/js/inc/core/server.js
index 759b81bb..3ca818f6 100644
--- a/src/_h5ai/client/js/inc/core/server.js
+++ b/src/_h5ai/client/js/inc/core/server.js
@@ -1,5 +1,5 @@
 
-modulejs.define('core/server', ['$', '_', 'config'], function ($, _, config) {
+modulejs.define('core/server', ['$', '_', 'config', 'core/location'], function ($, _, config, location) {
 
 	var server = _.extend({}, config.server, {
 
@@ -7,7 +7,7 @@ modulejs.define('core/server', ['$', '_', 'config'], function ($, _, config) {
 
 			if (server.api) {
 				$.ajax({
-					url: '.',
+					url: location.getAbsHref(),
 					data: data,
 					type: 'POST',
 					dataType: 'json',
diff --git a/src/_h5ai/client/js/inc/ext/crumb.js b/src/_h5ai/client/js/inc/ext/crumb.js
index da128980..72ebfbd3 100644
--- a/src/_h5ai/client/js/inc/ext/crumb.js
+++ b/src/_h5ai/client/js/inc/ext/crumb.js
@@ -1,5 +1,5 @@
 
-modulejs.define('ext/crumb', ['_', '$', 'core/settings', 'core/resource', 'core/event', 'core/entry'], function (_, $, allsettings, resource, event, entry) {
+modulejs.define('ext/crumb', ['_', '$', 'core/settings', 'core/resource', 'core/event', 'core/location'], function (_, $, allsettings, resource, event, location) {
 
 	var settings = _.extend({
 			enabled: false
@@ -26,11 +26,11 @@ modulejs.define('ext/crumb', ['_', '$', 'core/settings', 'core/resource', 'core/
 
 			$html
 				.addClass(entry.isFolder() ? 'folder' : 'file')
+				.data('item', entry)
 				.data('status', entry.status);
 
-			$a
-				.attr('href', entry.absHref)
-				.find('span').text(entry.label).end();
+			location.setLink($a, entry);
+			$a.find('span').text(entry.label).end();
 
 			if (entry.isDomain()) {
 				$html.addClass('domain');
@@ -69,25 +69,49 @@ modulejs.define('ext/crumb', ['_', '$', 'core/settings', 'core/resource', 'core/
 			}
 		},
 
-		// creates the complete crumb from entry down to the root
-		init = function (entry) {
+		onLocationChanged = function (item) {
+
+			var crumb = item.getCrumb(),
+				$ul = $('#navbar'),
+				found = false;
+
+			$ul.find('.crumb').each(function () {
+
+				var $html = $(this);
+				if ($html.data('item') === item) {
+					found = true;
+					$html.addClass('current');
+				} else {
+					$html.removeClass('current');
+				}
+			});
+
+			if (!found) {
+				$ul.find('.crumb').remove();
+				_.each(crumb, function (e) {
+
+					$ul.append(update(e));
+				});
+			}
+		},
+
+		init = function () {
 
 			if (!settings.enabled) {
 				return;
 			}
 
-			var crumb = entry.getCrumb(),
-				$ul = $('#navbar');
-
-			_.each(crumb, function (e) {
-
-				$ul.append(update(e));
-			});
-
 			// event.sub('entry.created', onContentChanged);
 			// event.sub('entry.removed', onContentChanged);
 			event.sub('entry.changed', onContentChanged);
+
+			event.sub('location.changed', function () {
+
+				onLocationChanged(location.getItem());
+			});
+
+			onLocationChanged(location.getItem());
 		};
 
-	init(entry);
+	init();
 });
diff --git a/src/_h5ai/client/js/inc/ext/custom.js b/src/_h5ai/client/js/inc/ext/custom.js
index 5da2495b..649fcc70 100644
--- a/src/_h5ai/client/js/inc/ext/custom.js
+++ b/src/_h5ai/client/js/inc/ext/custom.js
@@ -1,5 +1,5 @@
 
-modulejs.define('ext/custom', ['_', '$', 'core/settings', 'core/server'], function (_, $, allsettings, server) {
+modulejs.define('ext/custom', ['_', '$', 'core/settings', 'core/server', 'core/event'], function (_, $, allsettings, server, event) {
 
 	var settings = _.extend({
 			enabled: false,
@@ -7,23 +7,35 @@ modulejs.define('ext/custom', ['_', '$', 'core/settings', 'core/server'], functi
 			footer: '_h5ai.footer.html'
 		}, allsettings.custom),
 
+		onLocationChanged = function () {
+
+			$('#content-header, #content-footer').stop(true, true).slideUp(200);
+
+			server.request({action: 'get', custom: true}, function (response) {
+
+				if (response) {
+					if (response.custom.header) {
+						$('#content-header').html(response.custom.header).stop(true, true).slideDown(400);
+					}
+					if (response.custom.footer) {
+						$('#content-footer').html(response.custom.footer).stop(true, true).slideDown(400);
+					}
+				}
+			});
+		},
+
 		init = function () {
 
 			if (!settings.enabled) {
 				return;
 			}
 
-			server.request({action: 'get', custom: true}, function (response) {
+			$('<div id="content-header"/>').hide().prependTo('#content');
+			$('<div id="content-footer"/>').hide().appendTo('#content');
 
-				if (response) {
-					if (response.custom.header) {
-						$('<div id="content-header">' + response.custom.header + '</div>').prependTo('#content');
-					}
-					if (response.custom.footer) {
-						$('<div id="content-footer">' + response.custom.footer + '</div>').appendTo('#content');
-					}
-				}
-			});
+			event.sub('location.changed', onLocationChanged);
+
+			onLocationChanged();
 		};
 
 	init();
diff --git a/src/_h5ai/client/js/inc/ext/link-hover-states.js b/src/_h5ai/client/js/inc/ext/link-hover-states.js
index f3a84e0c..ee4eb910 100644
--- a/src/_h5ai/client/js/inc/ext/link-hover-states.js
+++ b/src/_h5ai/client/js/inc/ext/link-hover-states.js
@@ -1,5 +1,5 @@
 
-modulejs.define('ext/link-hover-states', ['_', '$', 'core/settings'], function (_, $, allsettings) {
+modulejs.define('ext/link-hover-states', ['_', '$', 'core/settings', 'core/event'], function (_, $, allsettings, event) {
 
 	var settings = _.extend({
 			enabled: false
@@ -29,13 +29,22 @@ modulejs.define('ext/link-hover-states', ['_', '$', 'core/settings'], function (
 			selectLinks(href).removeClass('hover');
 		},
 
+		onLocationChanged = function () {
+
+			$('.hover').removeClass('hover');
+		},
+
 		init = function () {
 
-			if (settings.enabled) {
-				$('body')
-					.on('mouseenter', selector, onMouseEnter)
-					.on('mouseleave', selector, onMouseLeave);
+			if (!settings.enabled) {
+				return;
 			}
+
+			$('body')
+				.on('mouseenter', selector, onMouseEnter)
+				.on('mouseleave', selector, onMouseLeave);
+
+			event.sub('location.changed', onLocationChanged);
 		};
 
 	init();
diff --git a/src/_h5ai/client/js/inc/main.js b/src/_h5ai/client/js/inc/main.js
index ed29852e..04e7a1cd 100644
--- a/src/_h5ai/client/js/inc/main.js
+++ b/src/_h5ai/client/js/inc/main.js
@@ -3,10 +3,12 @@ modulejs.define('main', ['_', 'core/event'], function (_, event) {
 
 	event.pub('beforeView');
 
-	modulejs.require('view/extended');
+	modulejs.require('view/items');
 	modulejs.require('view/spacing');
 	modulejs.require('view/viewmode');
 
+	modulejs.require('core/location').setLocation(document.location.href, true);
+
 	event.pub('beforeExt');
 
 	_.each(modulejs.state(), function (state, id) {
diff --git a/src/_h5ai/client/js/inc/model/entry.js b/src/_h5ai/client/js/inc/model/entry.js
index 21ebf124..d1e3cb6e 100644
--- a/src/_h5ai/client/js/inc/model/entry.js
+++ b/src/_h5ai/client/js/inc/model/entry.js
@@ -1,5 +1,6 @@
 
-modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/settings', 'core/location', 'core/server'], function ($, _, types, event, settings, location, server) {
+modulejs.define('model/entry', ['$', '_', 'config', 'core/types', 'core/event', 'core/settings', 'core/server', 'core/location'], function ($, _, config, types, event, settings, server, location) {
+
 
 	var reEndsWithSlash = /\/$/,
 
@@ -44,7 +45,7 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 
 		getEntry = function (absHref, time, size, status, isContentFetched) {
 
-			absHref = location.forceEncoding(absHref || location.absHref);
+			absHref = location.forceEncoding(absHref);
 
 			if (!startsWith(absHref, settings.rootAbsHref)) {
 				return null;
@@ -88,7 +89,7 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 
 		removeEntry = function (absHref) {
 
-			absHref = location.forceEncoding(absHref || location.absHref);
+			absHref = location.forceEncoding(absHref);
 
 			var self = cache[absHref];
 
@@ -128,6 +129,14 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 					callback(self);
 				});
 			}
+		},
+
+		init = function () {
+
+			_.each(config.entries || [], function (entry) {
+
+				getEntry(entry.absHref, entry.time, entry.size, entry.status, entry.content);
+			});
 		};
 
 
@@ -142,7 +151,7 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 
 		this.absHref = absHref;
 		this.type = types.getType(absHref);
-		this.label = createLabel(absHref === '/' ? location.domain : split.name);
+		this.label = createLabel(absHref === '/' ? location.getDomain() : split.name);
 		this.time = null;
 		this.size = null;
 		this.parent = null;
@@ -167,7 +176,7 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 
 		isCurrentFolder: function () {
 
-			return this.absHref === location.absHref;
+			return this.absHref === location.getAbsHref();
 		},
 
 		isInCurrentFolder: function () {
@@ -177,7 +186,7 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 
 		isCurrentParentFolder: function () {
 
-			return this === getEntry().parent;
+			return this === getEntry(location.getAbsHref()).parent;
 		},
 
 		isDomain: function () {
@@ -259,6 +268,9 @@ modulejs.define('model/entry', ['$', '_', 'core/types', 'core/event', 'core/sett
 		}
 	});
 
+
+	init();
+
 	return {
 		get: getEntry,
 		remove: removeEntry
diff --git a/src/_h5ai/client/js/inc/view/extended.js b/src/_h5ai/client/js/inc/view/items.js
similarity index 75%
rename from src/_h5ai/client/js/inc/view/extended.js
rename to src/_h5ai/client/js/inc/view/items.js
index aecbe811..506bd301 100644
--- a/src/_h5ai/client/js/inc/view/extended.js
+++ b/src/_h5ai/client/js/inc/view/items.js
@@ -1,5 +1,5 @@
 
-modulejs.define('view/extended', ['_', '$', 'core/settings', 'core/resource', 'core/format', 'core/event', 'core/entry'], function (_, $, allsettings, resource, format, event, entry) {
+modulejs.define('view/items', ['_', '$', 'core/settings', 'core/resource', 'core/format', 'core/event', 'core/location'], function (_, $, allsettings, resource, format, event, location) {
 
 	var settings = _.extend({
 			setParentFolderLabels: false,
@@ -25,6 +25,7 @@ modulejs.define('view/extended', ['_', '$', 'core/settings', 'core/resource', 'c
 							'</li>' +
 						'</ul>',
 		emptyTemplate = '<div class="empty l10n-empty"/>',
+		contentTemplate = '<div id="content"><div id="extended" class="clearfix"/></div>',
 
 		// updates this single entry
 		update = function (entry, force) {
@@ -40,14 +41,14 @@ modulejs.define('view/extended', ['_', '$', 'core/settings', 'core/resource', 'c
 				$label = $html.find('.label'),
 				$date = $html.find('.date'),
 				$size = $html.find('.size');
-				// escapedHref = entry.absHref.replace(/'/g, "%27").replace(/"/g, "%22");
 
 			$html
 				.addClass(entry.isFolder() ? 'folder' : 'file')
 				.data('entry', entry)
 				.data('status', entry.status);
 
-			$a.attr('href', entry.absHref);
+			location.setLink($a, entry);
+
 			$imgSmall.attr('src', resource.icon(entry.type)).attr('alt', entry.type);
 			$imgBig.attr('src', resource.icon(entry.type, true)).attr('alt', entry.type);
 			$label.text(entry.label);
@@ -94,32 +95,42 @@ modulejs.define('view/extended', ['_', '$', 'core/settings', 'core/resource', 'c
 			event.pub('entry.mouseleave', entry);
 		},
 
-		// creates the view for entry content
-		init = function (entry) {
+		onLocationChanged = function (item) {
 
 			var $extended = $('#extended'),
-				$ul = $(listTemplate),
-				$emtpy = $(emptyTemplate);
+				$ul = $extended.find('ul'),
+				$empty = $extended.find('.empty');
 
-			format.setDefaultMetric(settings.binaryPrefix);
+			$ul.find('.entry').remove();
 
-			if (entry.parent) {
-				$ul.append(update(entry.parent));
+			if (item.parent) {
+				$ul.append(update(item.parent));
 			}
 
-			_.each(entry.content, function (e) {
+			_.each(item.content, function (e) {
 
 				$ul.append(update(e));
 			});
 
-			$extended.append($ul);
-			$extended.append($emtpy);
-
-			if (!entry.isEmpty()) {
-				$emtpy.hide();
+			if (item.isEmpty()) {
+				$empty.show();
+			} else {
+				$empty.hide();
 			}
+		},
+
+		init = function () {
+
+			var $content = $(contentTemplate),
+				$extended = $content.find('#extended'),
+				$ul = $(listTemplate),
+				$emtpy = $(emptyTemplate).hide();
+
+			format.setDefaultMetric(settings.binaryPrefix);
 
 			$extended
+				.append($ul)
+				.append($emtpy)
 				.on('mouseenter', '.entry a', onMouseenter)
 				.on('mouseleave', '.entry a', onMouseleave);
 
@@ -133,24 +144,32 @@ modulejs.define('view/extended', ['_', '$', 'core/settings', 'core/resource', 'c
 			event.sub('entry.created', function (entry) {
 
 				if (entry.isInCurrentFolder() && !entry.$extended) {
-					update(entry, true).hide().appendTo($ul).slideDown(400);
-					$emtpy.slideUp(400);
+					$emtpy.fadeOut(100, function () {
+						update(entry, true).hide().appendTo($ul).fadeIn(400);
+					});
 				}
 			});
 
 			event.sub('entry.removed', function (entry) {
 
 				if (entry.isInCurrentFolder() && entry.$extended) {
-					entry.$extended.slideUp(400, function () {
+					entry.$extended.fadeOut(400, function () {
 						entry.$extended.remove();
 
 						if (entry.parent.isEmpty()) {
-							$emtpy.slideDown(400);
+							$emtpy.fadeIn(100);
 						}
 					});
 				}
 			});
+
+			event.sub('location.changed', function () {
+
+				onLocationChanged(location.getItem());
+			});
+
+			$content.appendTo('body');
 		};
 
-	init(entry);
+	init();
 });
diff --git a/src/_h5ai/conf/options.json b/src/_h5ai/conf/options.json
index ec68c0b6..dfbef51b 100644
--- a/src/_h5ai/conf/options.json
+++ b/src/_h5ai/conf/options.json
@@ -38,7 +38,8 @@ Options
 		"setParentFolderLabels": true,
 		"binaryPrefix": false,
 		"indexFiles": ["index.html", "index.htm", "index.php"],
-		"ignore": ["^\\.", "^_{{pkg.name}}"]
+		"ignore": ["^\\.", "^_{{pkg.name}}"],
+		"smartBrowsing": true
 	},
 
 
@@ -67,7 +68,7 @@ Options
 	in each folder.
 	*/
 	"custom": {
-		"enabled": false,
+		"enabled": true,
 		"header": "_{{pkg.name}}.header.html",
 		"footer": "_{{pkg.name}}.footer.html"
 	},
diff --git a/src/_h5ai/server/php/index.php.jade b/src/_h5ai/server/php/index.php.jade
index c9f0bd2d..8ea6cbe1 100644
--- a/src/_h5ai/server/php/index.php.jade
+++ b/src/_h5ai/server/php/index.php.jade
@@ -27,9 +27,6 @@ html.no-js( lang="en" )
 		div#topbar.clearfix
 			ul#navbar
 
-		div#content
-			div#extended.clearfix
-
 		div#bottombar.clearfix
 			span.left
 				a#h5ai-reference( href="{{pkg.url}}", title="{{pkg.name}} ยท {{pkg.description}}" )