diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 720ca29434..b70dc4e070 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -317,6 +317,7 @@
+
@@ -349,6 +350,8 @@
+
+
@@ -924,6 +927,7 @@
+
@@ -1799,6 +1803,7 @@
+
diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config
index 3cc7f041af..432fc7c806 100644
--- a/src/Umbraco.Web.UI/config/Dashboard.config
+++ b/src/Umbraco.Web.UI/config/Dashboard.config
@@ -30,6 +30,11 @@
media
+
+
+ /umbraco/dashboard/mediadashboardfolderbrowser.ascx
+
+
/umbraco/dashboard/mediadashboardintro.ascx
diff --git a/src/Umbraco.Web.UI/umbraco/dashboard/MediaDashboardFolderBrowser.ascx b/src/Umbraco.Web.UI/umbraco/dashboard/MediaDashboardFolderBrowser.ascx
new file mode 100644
index 0000000000..b1c7f72b46
--- /dev/null
+++ b/src/Umbraco.Web.UI/umbraco/dashboard/MediaDashboardFolderBrowser.ascx
@@ -0,0 +1,4 @@
+<%@ Control Language="C#" AutoEventWireup="true" %>
+<%@ Register TagPrefix="umb" Namespace="umbraco.uicontrols.FolderBrowser" Assembly="controls" %>
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Css/folderbrowser.css b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Css/folderbrowser.css
new file mode 100644
index 0000000000..12c9f0bc8a
--- /dev/null
+++ b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Css/folderbrowser.css
@@ -0,0 +1,60 @@
+.umbFolderBrowser {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.umbFolderBrowser .breadcrumb {
+ margin: 20px 20px 15px;
+ padding: 0 0 10px;
+
+ border-bottom: solid 1px #ccc;
+}
+
+.umbFolderBrowser .breadcrumb li {
+ display: inline;
+ padding: 0 5px 0 0;
+ margin: 0;
+}
+
+.umbFolderBrowser .breadcrumb li:after {
+ content: '>';
+}
+
+.umbFolderBrowser .breadcrumb li:last-child:after,
+.umbFolderBrowser .breadcrumb li:first-child:after {
+ content: '';
+}
+
+.umbFolderBrowser .breadcrumb li a {
+ margin-right: 5px;
+}
+
+.umbFolderBrowser .items {
+ margin: 5px 15px;
+ padding: 0;
+}
+
+.umbFolderBrowser .items li {
+ display: block;
+ list-style: none;
+ margin: 5px;
+ padding: 0;
+ background: #f5f5f5;
+ text-align: center;
+ border: solid 1px #ccc;
+ width: 120px;
+ height: 160px;
+ float: left;
+ -moz-box-shadow: 2px 2px 2px #e0e0e0;
+ -webkit-box-shadow: 2px 2px 2px #e0e0e0;
+ box-shadow: 2px 2px 2px #e0e0e0;
+}
+
+.umbFolderBrowser .items li img {
+ width: 100px;
+ height: 100px;
+ margin: 10px;
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js
new file mode 100644
index 0000000000..c37a364047
--- /dev/null
+++ b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js
@@ -0,0 +1,98 @@
+
+Umbraco.Sys.registerNamespace("Umbraco.Controls");
+
+(function ($, Base) {
+
+ Umbraco.Controls.FolderBrowser = Base.extend({
+
+ // Private
+ _el: null,
+ _elId: null,
+ _parentId: null,
+ _opts: null,
+ _viewModel: null,
+
+ // Constructor
+ constructor: function (el, opts) {
+
+ _this = this;
+
+ // Store el info
+ this._el = el;
+ this._elId = el.id;
+
+ // Grab parent id from element
+ this._parentId = $(el).data("parentid");
+
+ // Merge options with default
+ this._opts = $.extend({
+ // Default options go here
+ }, opts);
+
+ // Setup the viewmode;
+ this._viewModel = $.extend({}, {
+ parent: this,
+ panelVisible: ko.observable(true),
+ items: ko.observableArray([])
+ });
+
+ // Inject the upload button into the toolbar
+ var button = $("");
+ button.click(function(e) {
+ e.preventDefault();
+ });
+
+ $(".tabpage:first-child .menubar td[id$='tableContainerButtons'] .sl nobr").after(button);
+
+ // Grab children media items
+ this._viewModel.items.push({ name: "test1.jpg" });
+ this._viewModel.items.push({ name: "test2.jpg" });
+ this._viewModel.items.push({ name: "test3.jpg" });
+ this._viewModel.items.push({ name: "test4.jpg" });
+ this._viewModel.items.push({ name: "test5.jpg" });
+ this._viewModel.items.push({ name: "test1.jpg" });
+ this._viewModel.items.push({ name: "test2.jpg" });
+ this._viewModel.items.push({ name: "test3.jpg" });
+ this._viewModel.items.push({ name: "test4.jpg" });
+ this._viewModel.items.push({ name: "test5.jpg" });
+
+ // Bind the viewmodel
+ ko.applyBindings(this._viewModel, el);
+ }
+
+ // Public
+
+ });
+
+ $.fn.folderBrowser = function (o)
+ {
+ if ($(this).length != 1) {
+ throw "Only one folder browser can exist on the page at any one time";
+ }
+
+ return $(this).each(function () {
+ var folderBrowser = new Umbraco.Controls.FolderBrowser(this, o);
+ $(this).data("api", folderBrowser);
+ });
+ };
+
+ $.fn.folderBrowserApi = function ()
+ {
+ //ensure there's only 1
+ if ($(this).length != 1) {
+ throw "Requesting the API can only match one element";
+ }
+
+ //ensure thsi is a collapse panel
+ if ($(this).data("api") == null) {
+ throw "The matching element had not been bound to a folderBrowser";
+ }
+
+ return $(this).data("api");
+ };
+
+ $(function () {
+ $(".umbFolderBrowser").folderBrowser();
+ });
+
+})(jQuery, base2.Base)
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI/umbraco_client/ui/base2.js b/src/Umbraco.Web.UI/umbraco_client/ui/base2.js
new file mode 100644
index 0000000000..80ae31de03
--- /dev/null
+++ b/src/Umbraco.Web.UI/umbraco_client/ui/base2.js
@@ -0,0 +1,1675 @@
+/*
+ base2 - copyright 2007-2011, Dean Edwards
+ http://code.google.com/p/base2/
+ http://www.opensource.org/licenses/mit-license.php
+
+ Contributors:
+ Doeke Zanstra
+*/
+
+var base2 = {
+ name: "base2",
+ version: "1.0.2",
+ exports:
+ "Base,Package,Abstract,Module,Enumerable,Map,Collection,RegGrp," +
+ "Undefined,Null,This,True,False,assignID,detect,global",
+ namespace: ""
+};
+
+new function(_no_shrink_) { /////////////// BEGIN: CLOSURE ///////////////
+
+// =========================================================================
+// base2/header.js
+// =========================================================================
+
+var Undefined = K(), Null = K(null), True = K(true), False = K(false), This = function(){return this};
+
+var global = This();
+var base2 = global.base2;
+
+// private
+var _FORMAT = /%([1-9])/g;
+var _LTRIM = /^\s\s*/;
+var _RTRIM = /\s\s*$/;
+var _RESCAPE = /([\/()[\]{}|*+-.,^$?\\])/g; // safe regular expressions
+var _BASE = /try/.test(detect) ? /\bbase\b/ : /.*/; // some platforms don't allow decompilation
+var _HIDDEN = ["constructor", "toString", "valueOf"]; // only override these when prototyping
+var _MSIE_NATIVE_FUNCTION = detect("(jscript)") ?
+ new RegExp("^" + rescape(isNaN).replace(/isNaN/, "\\w+") + "$") : {test: False};
+
+var _counter = 1;
+var _slice = Array.prototype.slice;
+
+_Function_forEach(); // make sure this is initialised
+
+function assignID(object) {
+ // Assign a unique ID to an object.
+ if (!object.base2ID) object.base2ID = "b2_" + _counter++;
+ return object.base2ID;
+};
+
+// =========================================================================
+// base2/Base.js
+// =========================================================================
+
+// http://dean.edwards.name/weblog/2006/03/base/
+
+var _subclass = function(_instance, _static) {
+ // Build the prototype.
+ base2.__prototyping = this.prototype;
+ var _prototype = new this;
+ if (_instance) extend(_prototype, _instance);
+ delete base2.__prototyping;
+
+ // Create the wrapper for the constructor function.
+ var _constructor = _prototype.constructor;
+ function _class() {
+ // Don't call the constructor function when prototyping.
+ if (!base2.__prototyping) {
+ if (this.constructor == arguments.callee || this.__constructing) {
+ // Instantiation.
+ this.__constructing = true;
+ _constructor.apply(this, arguments);
+ delete this.__constructing;
+ } else {
+ // Casting.
+ return extend(arguments[0], _prototype);
+ }
+ }
+ return this;
+ };
+ _prototype.constructor = _class;
+
+ // Build the static interface.
+ for (var i in Base) _class[i] = this[i];
+ _class.ancestor = this;
+ _class.base = Undefined;
+ //_class.init = Undefined;
+ if (_static) extend(_class, _static);
+ _class.prototype = _prototype;
+ if (_class.init) _class.init();
+
+ // introspection (removed when packed)
+ ;;; _class["#implements"] = [];
+ ;;; _class["#implemented_by"] = [];
+
+ return _class;
+};
+
+var Base = _subclass.call(Object, {
+ constructor: function() {
+ if (arguments.length > 0) {
+ this.extend(arguments[0]);
+ }
+ },
+
+ base: function() {
+ // Call this method from any other method to invoke the current method's ancestor (super).
+ },
+
+ extend: delegate(extend)
+}, Base = {
+ ancestorOf: function(klass) {
+ return _ancestorOf(this, klass);
+ },
+
+ extend: _subclass,
+
+ forEach: function(object, block, context) {
+ _Function_forEach(this, object, block, context);
+ },
+
+ implement: function(source) {
+ if (typeof source == "function") {
+ ;;; if (_ancestorOf(Base, source)) {
+ // introspection (removed when packed)
+ ;;; this["#implements"].push(source);
+ ;;; source["#implemented_by"].push(this);
+ ;;; }
+ source = source.prototype;
+ }
+ // Add the interface using the extend() function.
+ extend(this.prototype, source);
+ return this;
+ }
+});
+
+// =========================================================================
+// base2/Package.js
+// =========================================================================
+
+var Package = Base.extend({
+ constructor: function(_private, _public) {
+ this.extend(_public);
+ if (this.init) this.init();
+
+ if (this.name && this.name != "base2") {
+ if (!this.parent) this.parent = base2;
+ this.parent.addName(this.name, this);
+ this.namespace = format("var %1=%2;", this.name, String2.slice(this, 1, -1));
+ }
+
+ if (_private) {
+ // This next line gets round a bug in old Mozilla browsers
+ var JSNamespace = base2.JavaScript ? base2.JavaScript.namespace : "";
+ // This string should be evaluated immediately after creating a Package object.
+ _private.imports = Array2.reduce(csv(this.imports), function(namespace, name) {
+ var ns = lookup(name) || lookup("JavaScript." + name);
+ ;;; assert(ns, format("Object not found: '%1'.", name), ReferenceError);
+ return namespace += ns.namespace;
+ }, "var base2=(function(){return this.base2})();" + base2.namespace + JSNamespace) + lang.namespace;
+
+ // This string should be evaluated after you have created all of the objects
+ // that are being exported.
+ _private.exports = Array2.reduce(csv(this.exports), function(namespace, name) {
+ var fullName = this.name + "." + name;
+ this.namespace += "var " + name + "=" + fullName + ";";
+ return namespace += "if(!" + fullName + ")" + fullName + "=" + name + ";";
+ }, "", this) + "this._label_" + this.name + "();";
+
+ var pkg = this;
+ var packageName = String2.slice(this, 1, -1);
+ _private["_label_" + this.name] = function() {
+ Package.forEach (pkg, function(object, name) {
+ if (object && object.ancestorOf == Base.ancestorOf) {
+ object.toString = K(format("[%1.%2]", packageName, name));
+ if (object.prototype.toString == Base.prototype.toString) {
+ object.prototype.toString = K(format("[object %1.%2]", packageName, name));
+ }
+ }
+ });
+ };
+ }
+
+ function lookup(names) {
+ names = names.split(".");
+ var value = base2, i = 0;
+ while (value && names[i] != null) {
+ value = value[names[i++]];
+ }
+ return value;
+ };
+ },
+
+ exports: "",
+ imports: "",
+ name: "",
+ namespace: "",
+ parent: null,
+
+ addName: function(name, value) {
+ if (!this[name]) {
+ this[name] = value;
+ this.exports += "," + name;
+ this.namespace += format("var %1=%2.%1;", name, this.name);
+ }
+ },
+
+ addPackage: function(name) {
+ this.addName(name, new Package(null, {name: name, parent: this}));
+ },
+
+ toString: function() {
+ return format("[%1]", this.parent ? String2.slice(this.parent, 1, -1) + "." + this.name : this.name);
+ }
+});
+
+// =========================================================================
+// base2/Abstract.js
+// =========================================================================
+
+var Abstract = Base.extend({
+ constructor: function() {
+ throw new TypeError("Abstract class cannot be instantiated.");
+ }
+});
+
+// =========================================================================
+// base2/Module.js
+// =========================================================================
+
+var _moduleCount = 0;
+
+var Module = Abstract.extend(null, {
+ namespace: "",
+
+ extend: function(_interface, _static) {
+ // Extend a module to create a new module.
+ var module = this.base();
+ var index = _moduleCount++;
+ module.namespace = "";
+ module.partial = this.partial;
+ module.toString = K("[base2.Module[" + index + "]]");
+ Module[index] = module;
+ // Inherit class methods.
+ module.implement(this);
+ // Implement module (instance AND static) methods.
+ if (_interface) module.implement(_interface);
+ // Implement static properties and methods.
+ if (_static) {
+ extend(module, _static);
+ if (module.init) module.init();
+ }
+ return module;
+ },
+
+ forEach: function(block, context) {
+ _Function_forEach (Module, this.prototype, function(method, name) {
+ if (typeOf(method) == "function") {
+ block.call(context, this[name], name, this);
+ }
+ }, this);
+ },
+
+ implement: function(_interface) {
+ var module = this;
+ var id = module.toString().slice(1, -1);
+ if (typeof _interface == "function") {
+ if (!_ancestorOf(_interface, module)) {
+ this.base(_interface);
+ }
+ if (_ancestorOf(Module, _interface)) {
+ // Implement static methods.
+ for (var name in _interface) {
+ if (module[name] === undefined) {
+ var property = _interface[name];
+ if (typeof property == "function" && property.call && _interface.prototype[name]) {
+ property = _staticModuleMethod(_interface, name);
+ }
+ module[name] = property;
+ }
+ }
+ module.namespace += _interface.namespace.replace(/base2\.Module\[\d+\]/g, id);
+ }
+ } else {
+ // Add static interface.
+ extend(module, _interface);
+ // Add instance interface.
+ _extendModule(module, _interface);
+ }
+ return module;
+ },
+
+ partial: function() {
+ var module = Module.extend();
+ var id = module.toString().slice(1, -1);
+ // partial methods are already bound so remove the binding to speed things up
+ module.namespace = this.namespace.replace(/(\w+)=b[^\)]+\)/g, "$1=" + id + ".$1");
+ this.forEach(function(method, name) {
+ module[name] = partial(bind(method, module));
+ });
+ return module;
+ }
+});
+
+function _extendModule(module, _interface) {
+ var proto = module.prototype;
+ var id = module.toString().slice(1, -1);
+ for (var name in _interface) {
+ var property = _interface[name], namespace = "";
+ if (name.charAt(0) == "@") { // object detection
+ if (detect(name.slice(1))) _extendModule(module, property);
+ } else if (!proto[name]) {
+ if (name == name.toUpperCase()) {
+ namespace = "var " + name + "=" + id + "." + name + ";";
+ } else if (typeof property == "function" && property.call) {
+ namespace = "var " + name + "=base2.lang.bind('" + name + "'," + id + ");";
+ proto[name] = _moduleMethod(module, name);
+ ;;; proto[name]._module = module; // introspection
+ }
+ if (module.namespace.indexOf(namespace) == -1) {
+ module.namespace += namespace;
+ }
+ }
+ }
+};
+
+function _staticModuleMethod(module, name) {
+ return function() {
+ return module[name].apply(module, arguments);
+ };
+};
+
+function _moduleMethod(module, name) {
+ return function() {
+ var args = _slice.call(arguments);
+ args.unshift(this);
+ return module[name].apply(module, args);
+ };
+};
+
+// =========================================================================
+// base2/Enumerable.js
+// =========================================================================
+
+var Enumerable = Module.extend({
+ every: function(object, test, context) {
+ var result = true;
+ try {
+ forEach (object, function(value, key) {
+ result = test.call(context, value, key, object);
+ if (!result) throw StopIteration;
+ });
+ } catch (error) {
+ if (error != StopIteration) throw error;
+ }
+ return !!result; // cast to boolean
+ },
+
+ filter: function(object, test, context) {
+ var i = 0;
+ return this.reduce(object, function(result, value, key) {
+ if (test.call(context, value, key, object)) {
+ result[i++] = value;
+ }
+ return result;
+ }, []);
+ },
+
+ invoke: function(object, method) {
+ // Apply a method to each item in the enumerated object.
+ var args = _slice.call(arguments, 2);
+ return this.map(object, (typeof method == "function") ? function(item) {
+ return item == null ? undefined : method.apply(item, args);
+ } : function(item) {
+ return item == null ? undefined : item[method].apply(item, args);
+ });
+ },
+
+ map: function(object, block, context) {
+ var result = [], i = 0;
+ forEach (object, function(value, key) {
+ result[i++] = block.call(context, value, key, object);
+ });
+ return result;
+ },
+
+ pluck: function(object, key) {
+ return this.map(object, function(item) {
+ return item == null ? undefined : item[key];
+ });
+ },
+
+ reduce: function(object, block, result, context) {
+ var initialised = arguments.length > 2;
+ forEach (object, function(value, key) {
+ if (initialised) {
+ result = block.call(context, result, value, key, object);
+ } else {
+ result = value;
+ initialised = true;
+ }
+ });
+ return result;
+ },
+
+ some: function(object, test, context) {
+ return !this.every(object, not(test), context);
+ }
+});
+
+// =========================================================================
+// base2/Map.js
+// =========================================================================
+
+// http://wiki.ecmascript.org/doku.php?id=proposals:dictionary
+
+var _HASH = "#";
+
+var Map = Base.extend({
+ constructor: function(values) {
+ if (values) this.merge(values);
+ },
+
+ clear: function() {
+ for (var key in this) if (key.indexOf(_HASH) == 0) {
+ delete this[key];
+ }
+ },
+
+ copy: function() {
+ base2.__prototyping = true; // not really prototyping but it stops [[construct]] being called
+ var copy = new this.constructor;
+ delete base2.__prototyping;
+ for (var i in this) if (this[i] !== copy[i]) {
+ copy[i] = this[i];
+ }
+ return copy;
+ },
+
+ forEach: function(block, context) {
+ for (var key in this) if (key.indexOf(_HASH) == 0) {
+ block.call(context, this[key], key.slice(1), this);
+ }
+ },
+
+ get: function(key) {
+ return this[_HASH + key];
+ },
+
+ getKeys: function() {
+ return this.map(II);
+ },
+
+ getValues: function() {
+ return this.map(I);
+ },
+
+ // Ancient browsers throw an error if we use "in" as an operator.
+ has: function(key) {
+ /*@cc_on @*/
+ /*@if (@_jscript_version < 5.5)
+ return $Legacy.has(this, _HASH + key);
+ @else @*/
+ return _HASH + key in this;
+ /*@end @*/
+ },
+
+ merge: function(values) {
+ var put = flip(this.put);
+ forEach (arguments, function(values) {
+ forEach (values, put, this);
+ }, this);
+ return this;
+ },
+
+ put: function(key, value) {
+ // create the new entry (or overwrite the old entry).
+ this[_HASH + key] = value;
+ },
+
+ remove: function(key) {
+ delete this[_HASH + key];
+ },
+
+ size: function() {
+ // this is expensive because we are not storing the keys
+ var size = 0;
+ for (var key in this) if (key.indexOf(_HASH) == 0) size++;
+ return size;
+ },
+
+ union: function(values) {
+ return this.merge.apply(this.copy(), arguments);
+ }
+});
+
+Map.implement(Enumerable);
+
+Map.prototype.filter = function(test, context) {
+ return this.reduce(function(result, value, key) {
+ if (!test.call(context, value, key, this)) {
+ result.remove(key);
+ }
+ return result;
+ }, this.copy(), this);
+};
+
+// =========================================================================
+// base2/Collection.js
+// =========================================================================
+
+// A Map that is more array-like (accessible by index).
+
+// Collection classes have a special (optional) property: Item
+// The Item property points to a constructor function.
+// Members of the collection must be an instance of Item.
+
+// The static create() method is responsible for all construction of collection items.
+// Instance methods that add new items (add, put, insertAt, putAt) pass *all* of their arguments
+// to the static create() method. If you want to modify the way collection items are
+// created then you only need to override this method for custom collections.
+
+var _KEYS = "~";
+
+var Collection = Map.extend({
+ constructor: function(values) {
+ this[_KEYS] = new Array2;
+ this.base(values);
+ },
+
+ add: function(key, item) {
+ // Duplicates not allowed using add().
+ // But you can still overwrite entries using put().
+ assert(!this.has(key), "Duplicate key '" + key + "'.");
+ this.put.apply(this, arguments);
+ },
+
+ clear: function() {
+ this.base();
+ this[_KEYS].length = 0;
+ },
+
+ copy: function() {
+ var copy = this.base();
+ copy[_KEYS] = this[_KEYS].copy();
+ return copy;
+ },
+
+ forEach: function(block, context) {
+ var keys = this[_KEYS];
+ var length = keys.length;
+ for (var i = 0; i < length; i++) {
+ block.call(context, this[_HASH + keys[i]], keys[i], this);
+ }
+ },
+
+ getAt: function(index) {
+ var key = this[_KEYS].item(index);
+ return (key === undefined) ? undefined : this[_HASH + key];
+ },
+
+ getKeys: function() {
+ return this[_KEYS].copy();
+ },
+
+ indexOf: function(key) {
+ return this[_KEYS].indexOf(String(key));
+ },
+
+ insertAt: function(index, key, item) {
+ assert(this[_KEYS].item(index) !== undefined, "Index out of bounds.");
+ assert(!this.has(key), "Duplicate key '" + key + "'.");
+ this[_KEYS].insertAt(index, String(key));
+ this[_HASH + key] = null; // placeholder
+ this.put.apply(this, _slice.call(arguments, 1));
+ },
+
+ item: function(keyOrIndex) {
+ return this[typeof keyOrIndex == "number" ? "getAt" : "get"](keyOrIndex);
+ },
+
+ put: function(key, item) {
+ if (!this.has(key)) {
+ this[_KEYS].push(String(key));
+ }
+ var klass = this.constructor;
+ if (klass.Item && !instanceOf(item, klass.Item)) {
+ item = klass.create.apply(klass, arguments);
+ }
+ this[_HASH + key] = item;
+ },
+
+ putAt: function(index, item) {
+ arguments[0] = this[_KEYS].item(index);
+ assert(arguments[0] !== undefined, "Index out of bounds.");
+ this.put.apply(this, arguments);
+ },
+
+ remove: function(key) {
+ // The remove() method of the Array object can be slow so check if the key exists first.
+ if (this.has(key)) {
+ this[_KEYS].remove(String(key));
+ delete this[_HASH + key];
+ }
+ },
+
+ removeAt: function(index) {
+ var key = this[_KEYS].item(index);
+ if (key !== undefined) {
+ this[_KEYS].removeAt(index);
+ delete this[_HASH + key];
+ }
+ },
+
+ reverse: function() {
+ this[_KEYS].reverse();
+ return this;
+ },
+
+ size: function() {
+ return this[_KEYS].length;
+ },
+
+ slice: function(start, end) {
+ var sliced = this.copy();
+ if (arguments.length > 0) {
+ var keys = this[_KEYS], removed = keys;
+ sliced[_KEYS] = Array2(_slice.apply(keys, arguments));
+ if (sliced[_KEYS].length) {
+ removed = removed.slice(0, start);
+ if (arguments.length > 1) {
+ removed = removed.concat(keys.slice(end));
+ }
+ }
+ for (var i = 0; i < removed.length; i++) {
+ delete sliced[_HASH + removed[i]];
+ }
+ }
+ return sliced;
+ },
+
+ sort: function(compare) { // optimised (refers to _HASH)
+ if (compare) {
+ this[_KEYS].sort(bind(function(key1, key2) {
+ return compare(this[_HASH + key1], this[_HASH + key2], key1, key2);
+ }, this));
+ } else this[_KEYS].sort();
+ return this;
+ },
+
+ toString: function() {
+ return "(" + (this[_KEYS] || "") + ")";
+ }
+}, {
+ Item: null, // If specified, all members of the collection must be instances of Item.
+
+ create: function(key, item) {
+ return this.Item ? new this.Item(key, item) : item;
+ },
+
+ extend: function(_instance, _static) {
+ var klass = this.base(_instance);
+ klass.create = this.create;
+ if (_static) extend(klass, _static);
+ if (!klass.Item) {
+ klass.Item = this.Item;
+ } else if (typeof klass.Item != "function") {
+ klass.Item = (this.Item || Base).extend(klass.Item);
+ }
+ if (klass.init) klass.init();
+ return klass;
+ }
+});
+
+// =========================================================================
+// base2/RegGrp.js
+// =========================================================================
+
+// A collection of regular expressions and their associated replacement values.
+// A Base class for creating parsers.
+
+var _RG_BACK_REF = /\\(\d+)/g,
+ _RG_ESCAPE_CHARS = /\\./g,
+ _RG_ESCAPE_BRACKETS = /\(\?[:=!]|\[[^\]]+\]/g,
+ _RG_BRACKETS = /\(/g,
+ _RG_LOOKUP = /\$(\d+)/,
+ _RG_LOOKUP_SIMPLE = /^\$\d+$/;
+
+var RegGrp = Collection.extend({
+ constructor: function(values, ignoreCase) {
+ this.base(values);
+ this.ignoreCase = !!ignoreCase;
+ },
+
+ ignoreCase: false,
+
+ exec: function(string, override) { // optimised (refers to _HASH/_KEYS)
+ string += ""; // type-safe
+ var items = this, keys = this[_KEYS];
+ if (!keys.length) return string;
+ if (override == RegGrp.IGNORE) override = 0;
+ return string.replace(new RegExp(this, this.ignoreCase ? "gi" : "g"), function(match) {
+ var item, offset = 1, i = 0;
+ // Loop through the RegGrp items.
+ while ((item = items[_HASH + keys[i++]])) {
+ var next = offset + item.length + 1;
+ if (arguments[offset]) { // do we have a result?
+ var replacement = override == null ? item.replacement : override;
+ switch (typeof replacement) {
+ case "function":
+ return replacement.apply(items, _slice.call(arguments, offset, next));
+ case "number":
+ return arguments[offset + replacement];
+ default:
+ return replacement;
+ }
+ }
+ offset = next;
+ }
+ return match;
+ });
+ },
+
+ insertAt: function(index, expression, replacement) {
+ if (instanceOf(expression, RegExp)) {
+ arguments[1] = expression.source;
+ }
+ return base(this, arguments);
+ },
+
+ test: function(string) {
+ // The slow way to do it. Hopefully, this isn't called too often. :-)
+ return this.exec(string) != string;
+ },
+
+ toString: function() {
+ var offset = 1;
+ return "(" + this.map(function(item) {
+ // Fix back references.
+ var expression = (item + "").replace(_RG_BACK_REF, function(match, index) {
+ return "\\" + (offset + Number(index));
+ });
+ offset += item.length + 1;
+ return expression;
+ }).join(")|(") + ")";
+ }
+}, {
+ IGNORE: "$0",
+
+ init: function() {
+ forEach ("add,get,has,put,remove".split(","), function(name) {
+ _override(this, name, function(expression) {
+ if (instanceOf(expression, RegExp)) {
+ arguments[0] = expression.source;
+ }
+ return base(this, arguments);
+ });
+ }, this.prototype);
+ },
+
+ Item: {
+ constructor: function(expression, replacement) {
+ if (replacement == null) replacement = RegGrp.IGNORE;
+ else if (replacement.replacement != null) replacement = replacement.replacement;
+ else if (typeof replacement != "function") replacement = String(replacement);
+
+ // does the pattern use sub-expressions?
+ if (typeof replacement == "string" && _RG_LOOKUP.test(replacement)) {
+ // a simple lookup? (e.g. "$2")
+ if (_RG_LOOKUP_SIMPLE.test(replacement)) {
+ // store the index (used for fast retrieval of matched strings)
+ replacement = parseInt(replacement.slice(1));
+ } else { // a complicated lookup (e.g. "Hello $2 $1")
+ // build a function to do the lookup
+ // Improved version by Alexei Gorkov:
+ var Q = '"';
+ replacement = replacement
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\x22")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/\$(\d+)/g, Q + "+(arguments[$1]||" + Q+Q + ")+" + Q)
+ .replace(/(['"])\1\+(.*)\+\1\1$/, "$1");
+ replacement = new Function("return " + Q + replacement + Q);
+ }
+ }
+
+ this.length = RegGrp.count(expression);
+ this.replacement = replacement;
+ this.toString = K(expression + "");
+ },
+
+ length: 0,
+ replacement: ""
+ },
+
+ count: function(expression) {
+ // Count the number of sub-expressions in a RegExp/RegGrp.Item.
+ expression = (expression + "").replace(_RG_ESCAPE_CHARS, "").replace(_RG_ESCAPE_BRACKETS, "");
+ return match(expression, _RG_BRACKETS).length;
+ }
+});
+
+// =========================================================================
+// lang/package.js
+// =========================================================================
+
+var lang = {
+ name: "lang",
+ version: base2.version,
+ exports: "assert,assertArity,assertType,base,bind,copy,extend,forEach,format,instanceOf,match,pcopy,rescape,trim,typeOf",
+ namespace: "" // fixed later
+};
+
+// =========================================================================
+// lang/assert.js
+// =========================================================================
+
+function assert(condition, message, ErrorClass) {
+ if (!condition) {
+ throw new (ErrorClass || Error)(message || "Assertion failed.");
+ }
+};
+
+function assertArity(args, arity, message) {
+ if (arity == null) arity = args.callee.length;
+ if (args.length < arity) {
+ throw new SyntaxError(message || "Not enough arguments.");
+ }
+};
+
+function assertType(object, type, message) {
+ if (type && (typeof type == "function" ? !instanceOf(object, type) : typeOf(object) != type)) {
+ throw new TypeError(message || "Invalid type.");
+ }
+};
+
+// =========================================================================
+// lang/copy.js
+// =========================================================================
+
+function copy(object) {
+ // a quick copy
+ var copy = {};
+ for (var i in object) {
+ copy[i] = object[i];
+ }
+ return copy;
+};
+
+function pcopy(object) {
+ // Doug Crockford / Richard Cornford
+ _dummy.prototype = object;
+ return new _dummy;
+};
+
+function _dummy(){};
+
+// =========================================================================
+// lang/extend.js
+// =========================================================================
+
+function base(object, args) {
+ return object.base.apply(object, args);
+};
+
+function extend(object, source) { // or extend(object, key, value)
+ if (object && source) {
+ if (arguments.length > 2) { // Extending with a key/value pair.
+ var key = source;
+ source = {};
+ source[key] = arguments[2];
+ }
+ var proto = global[(typeof source == "function" ? "Function" : "Object")].prototype;
+ // Add constructor, toString etc
+ if (base2.__prototyping) {
+ var i = _HIDDEN.length, key;
+ while ((key = _HIDDEN[--i])) {
+ var value = source[key];
+ if (value != proto[key]) {
+ if (_BASE.test(value)) {
+ _override(object, key, value)
+ } else {
+ object[key] = value;
+ }
+ }
+ }
+ }
+ // Copy each of the source object's properties to the target object.
+ for (key in source) {
+ if (proto[key] === undefined) {
+ var value = source[key];
+ // Object detection.
+ if (key.charAt(0) == "@") {
+ if (detect(key.slice(1))) extend(object, value);
+ } else {
+ // Check for method overriding.
+ var ancestor = object[key];
+ if (ancestor && typeof value == "function") {
+ if (value != ancestor) {
+ if (_BASE.test(value)) {
+ _override(object, key, value);
+ } else {
+ value.ancestor = ancestor;
+ object[key] = value;
+ }
+ }
+ } else {
+ object[key] = value;
+ }
+ }
+ }
+ }
+ }
+ return object;
+};
+
+function _ancestorOf(ancestor, fn) {
+ // Check if a function is in another function's inheritance chain.
+ while (fn) {
+ if (!fn.ancestor) return false;
+ fn = fn.ancestor;
+ if (fn == ancestor) return true;
+ }
+ return false;
+};
+
+function _override(object, name, method) {
+ // Override an existing method.
+ var ancestor = object[name];
+ var superObject = base2.__prototyping; // late binding for prototypes
+ if (superObject && ancestor != superObject[name]) superObject = null;
+ function _base() {
+ var previous = this.base;
+ this.base = superObject ? superObject[name] : ancestor;
+ var returnValue = method.apply(this, arguments);
+ this.base = previous;
+ return returnValue;
+ };
+ _base.method = method;
+ _base.ancestor = ancestor;
+ object[name] = _base;
+ // introspection (removed when packed)
+ ;;; _base.toString = K(method + "");
+};
+
+// =========================================================================
+// lang/forEach.js
+// =========================================================================
+
+// http://dean.edwards.name/weblog/2006/07/enum/
+
+if (typeof StopIteration == "undefined") {
+ StopIteration = new Error("StopIteration");
+}
+
+function forEach(object, block, context, fn) {
+ if (object == null) return;
+ if (!fn) {
+ if (typeof object == "function" && object.call) {
+ // Functions are a special case.
+ fn = Function;
+ } else if (typeof object.forEach == "function" && object.forEach != arguments.callee) {
+ // The object implements a custom forEach method.
+ object.forEach(block, context);
+ return;
+ } else if (typeof object.length == "number") {
+ // The object is array-like.
+ _Array_forEach(object, block, context);
+ return;
+ }
+ }
+ _Function_forEach(fn || Object, object, block, context);
+};
+
+forEach.csv = function(string, block, context) {
+ forEach (csv(string), block, context);
+};
+
+forEach.detect = function(object, block, context) {
+ forEach (object, function(value, key) {
+ if (key.charAt(0) == "@") { // object detection
+ if (detect(key.slice(1))) forEach (value, arguments.callee);
+ } else block.call(context, value, key, object);
+ });
+};
+
+// These are the two core enumeration methods. All other forEach methods
+// eventually call one of these two.
+
+function _Array_forEach(array, block, context) {
+ if (array == null) array = global;
+ var length = array.length || 0, i; // preserve length
+ if (typeof array == "string") {
+ for (i = 0; i < length; i++) {
+ block.call(context, array.charAt(i), i, array);
+ }
+ } else { // Cater for sparse arrays.
+ for (i = 0; i < length; i++) {
+ /*@cc_on @*/
+ /*@if (@_jscript_version < 5.2)
+ if ($Legacy.has(array, i))
+ @else @*/
+ if (i in array)
+ /*@end @*/
+ block.call(context, array[i], i, array);
+ }
+ }
+};
+
+function _Function_forEach(fn, object, block, context) {
+ // http://code.google.com/p/base2/issues/detail?id=10
+
+ // Run the test for Safari's buggy enumeration.
+ var Temp = function(){this.i=1};
+ Temp.prototype = {i:1};
+ var count = 0;
+ for (var i in new Temp) count++;
+
+ // Overwrite the main function the first time it is called.
+ _Function_forEach = (count > 1) ? function(fn, object, block, context) {
+ // Safari fix (pre version 3)
+ var processed = {};
+ for (var key in object) {
+ if (!processed[key] && fn.prototype[key] === undefined) {
+ processed[key] = true;
+ block.call(context, object[key], key, object);
+ }
+ }
+ } : function(fn, object, block, context) {
+ // Enumerate an object and compare its keys with fn's prototype.
+ for (var key in object) {
+ if (fn.prototype[key] === undefined) {
+ block.call(context, object[key], key, object);
+ }
+ }
+ };
+
+ _Function_forEach(fn, object, block, context);
+};
+
+// =========================================================================
+// lang/instanceOf.js
+// =========================================================================
+
+function instanceOf(object, klass) {
+ // Handle exceptions where the target object originates from another frame.
+ // This is handy for JSON parsing (amongst other things).
+
+ if (typeof klass != "function") {
+ throw new TypeError("Invalid 'instanceOf' operand.");
+ }
+
+ if (object == null) return false;
+
+ /*@cc_on
+ // COM objects don't have a constructor
+ if (typeof object.constructor != "function") {
+ return typeOf(object) == typeof klass.prototype.valueOf();
+ }
+ @*/
+ if (object.constructor == klass) return true;
+ if (klass.ancestorOf) return klass.ancestorOf(object.constructor);
+ /*@if (@_jscript_version < 5.1)
+ // do nothing
+ @else @*/
+ if (object instanceof klass) return true;
+ /*@end @*/
+
+ // If the class is a base2 class then it would have passed the test above.
+ if (Base.ancestorOf == klass.ancestorOf) return false;
+
+ // base2 objects can only be instances of Object.
+ if (Base.ancestorOf == object.constructor.ancestorOf) return klass == Object;
+
+ switch (klass) {
+ case Array: // This is the only troublesome one.
+ return !!(typeof object == "object" && object.join && object.splice);
+ case Function:
+ return typeOf(object) == "function";
+ case RegExp:
+ return typeof object.constructor.$1 == "string";
+ case Date:
+ return !!object.getTimezoneOffset;
+ case String:
+ case Number:
+ case Boolean:
+ return typeOf(object) == typeof klass.prototype.valueOf();
+ case Object:
+ return true;
+ }
+
+ return false;
+};
+
+// =========================================================================
+// lang/typeOf.js
+// =========================================================================
+
+// http://wiki.ecmascript.org/doku.php?id=proposals:typeof
+
+function typeOf(object) {
+ var type = typeof object;
+ switch (type) {
+ case "object":
+ return object == null
+ ? "null"
+ : typeof object.constructor == "undefined" // COM object
+ ? _MSIE_NATIVE_FUNCTION.test(object)
+ ? "function"
+ : type
+ : typeof object.constructor.prototype.valueOf(); // underlying type
+ case "function":
+ return typeof object.call == "function" ? type : "object";
+ default:
+ return type;
+ }
+};
+
+// =========================================================================
+// JavaScript/package.js
+// =========================================================================
+
+var JavaScript = {
+ name: "JavaScript",
+ version: base2.version,
+ exports: "Array2,Date2,Function2,String2",
+ namespace: "", // fixed later
+
+ bind: function(host) {
+ var top = global;
+ global = host;
+ forEach.csv(this.exports, function(name2) {
+ var name = name2.slice(0, -1);
+ extend(host[name], this[name2]);
+ this[name2](host[name].prototype); // cast
+ }, this);
+ global = top;
+ return host;
+ }
+};
+
+function _createObject2(Native, constructor, generics, extensions) {
+ // Clone native objects and extend them.
+
+ // Create a Module that will contain all the new methods.
+ var INative = Module.extend();
+ var id = INative.toString().slice(1, -1);
+ // http://developer.mozilla.org/en/docs/New_in_JavaScript_1.6#Array_and_String_generics
+ forEach.csv(generics, function(name) {
+ INative[name] = unbind(Native.prototype[name]);
+ INative.namespace += format("var %1=%2.%1;", name, id);
+ });
+ forEach (_slice.call(arguments, 3), INative.implement, INative);
+
+ // create a faux constructor that augments the native object
+ var Native2 = function() {
+ return INative(this.constructor == INative ? constructor.apply(null, arguments) : arguments[0]);
+ };
+ Native2.prototype = INative.prototype;
+
+ // Remove methods that are already implemented.
+ for (var name in INative) {
+ if (name != "prototype" && Native[name]) {
+ delete INative.prototype[name];
+ }
+ Native2[name] = INative[name];
+ }
+ Native2.ancestor = Object;
+ delete Native2.extend;
+
+ // remove "lang.bind.."
+ Native2.namespace = Native2.namespace.replace(/(var (\w+)=)[^,;]+,([^\)]+)\)/g, "$1$3.$2");
+
+ return Native2;
+};
+
+// =========================================================================
+// JavaScript/~/Date.js
+// =========================================================================
+
+// Fix Date.get/setYear() (IE5-7)
+
+if ((new Date).getYear() > 1900) {
+ Date.prototype.getYear = function() {
+ return this.getFullYear() - 1900;
+ };
+ Date.prototype.setYear = function(year) {
+ return this.setFullYear(year + 1900);
+ };
+}
+
+// https://bugs.webkit.org/show_bug.cgi?id=9532
+
+var _testDate = new Date(Date.UTC(2006, 1, 20));
+_testDate.setUTCDate(15);
+if (_testDate.getUTCHours() != 0) {
+ forEach.csv("FullYear,Month,Date,Hours,Minutes,Seconds,Milliseconds", function(type) {
+ extend(Date.prototype, "setUTC" + type, function() {
+ var value = base(this, arguments);
+ if (value >= 57722401000) {
+ value -= 3600000;
+ this.setTime(value);
+ }
+ return value;
+ });
+ });
+}
+
+// =========================================================================
+// JavaScript/~/Function.js
+// =========================================================================
+
+// Some browsers don't define this.
+
+Function.prototype.prototype = {};
+
+// =========================================================================
+// JavaScript/~/String.js
+// =========================================================================
+
+// A KHTML bug.
+
+if ("".replace(/^/, K("$$")) == "$") {
+ extend(String.prototype, "replace", function(expression, replacement) {
+ if (typeof replacement == "function") {
+ var fn = replacement;
+ replacement = function() {
+ return String(fn.apply(null, arguments)).split("$").join("$$");
+ };
+ }
+ return this.base(expression, replacement);
+ });
+}
+
+// =========================================================================
+// JavaScript/Array2.js
+// =========================================================================
+
+var Array2 = _createObject2(
+ Array,
+ Array,
+ "concat,join,pop,push,reverse,shift,slice,sort,splice,unshift", // generics
+ Enumerable, {
+ combine: function(keys, values) {
+ // Combine two arrays to make a hash.
+ if (!values) values = keys;
+ return Array2.reduce(keys, function(hash, key, index) {
+ hash[key] = values[index];
+ return hash;
+ }, {});
+ },
+
+ contains: function(array, item) {
+ return Array2.indexOf(array, item) != -1;
+ },
+
+ copy: function(array) {
+ var copy = _slice.call(array);
+ if (!copy.swap) Array2(copy); // cast to Array2
+ return copy;
+ },
+
+ flatten: function(array) {
+ var i = 0;
+ return Array2.reduce(array, function(result, item) {
+ if (Array2.like(item)) {
+ Array2.reduce(item, arguments.callee, result);
+ } else {
+ result[i++] = item;
+ }
+ return result;
+ }, []);
+ },
+
+ forEach: _Array_forEach,
+
+ indexOf: function(array, item, fromIndex) {
+ var length = array.length;
+ if (fromIndex == null) {
+ fromIndex = 0;
+ } else if (fromIndex < 0) {
+ fromIndex = Math.max(0, length + fromIndex);
+ }
+ for (var i = fromIndex; i < length; i++) {
+ if (array[i] === item) return i;
+ }
+ return -1;
+ },
+
+ insertAt: function(array, index, item) {
+ Array2.splice(array, index, 0, item);
+ return item;
+ },
+
+ item: function(array, index) {
+ if (index < 0) index += array.length; // starting from the end
+ return array[index];
+ },
+
+ lastIndexOf: function(array, item, fromIndex) {
+ var length = array.length;
+ if (fromIndex == null) {
+ fromIndex = length - 1;
+ } else if (fromIndex < 0) {
+ fromIndex = Math.max(0, length + fromIndex);
+ }
+ for (var i = fromIndex; i >= 0; i--) {
+ if (array[i] === item) return i;
+ }
+ return -1;
+ },
+
+ map: function(array, block, context) {
+ var result = [];
+ Array2.forEach (array, function(item, index) {
+ result[index] = block.call(context, item, index, array);
+ });
+ return result;
+ },
+
+ remove: function(array, item) {
+ var index = Array2.indexOf(array, item);
+ if (index != -1) Array2.removeAt(array, index);
+ },
+
+ removeAt: function(array, index) {
+ Array2.splice(array, index, 1);
+ },
+
+ swap: function(array, index1, index2) {
+ if (index1 < 0) index1 += array.length; // starting from the end
+ if (index2 < 0) index2 += array.length;
+ var temp = array[index1];
+ array[index1] = array[index2];
+ array[index2] = temp;
+ return array;
+ }
+ }
+);
+
+Array2.reduce = Enumerable.reduce; // Mozilla does not implement the thisObj argument
+
+Array2.like = function(object) {
+ // is the object like an array?
+ return typeOf(object) == "object" && typeof object.length == "number";
+};
+
+// introspection (removed when packed)
+;;; Enumerable["#implemented_by"].pop();
+;;; Enumerable["#implemented_by"].push(Array2);
+
+// =========================================================================
+// JavaScript/Date2.js
+// =========================================================================
+
+// http://developer.mozilla.org/es4/proposals/date_and_time.html
+
+// big, ugly, regular expression
+var _DATE_PATTERN = /^((-\d+|\d{4,})(-(\d{2})(-(\d{2}))?)?)?T((\d{2})(:(\d{2})(:(\d{2})(\.(\d{1,3})(\d)?\d*)?)?)?)?(([+-])(\d{2})(:(\d{2}))?|Z)?$/;
+var _DATE_PARTS = { // indexes to the sub-expressions of the RegExp above
+ FullYear: 2,
+ Month: 4,
+ Date: 6,
+ Hours: 8,
+ Minutes: 10,
+ Seconds: 12,
+ Milliseconds: 14
+};
+var _TIMEZONE_PARTS = { // idem, but without the getter/setter usage on Date object
+ Hectomicroseconds: 15, // :-P
+ UTC: 16,
+ Sign: 17,
+ Hours: 18,
+ Minutes: 20
+};
+
+var _TRIM_ZEROES = /(((00)?:0+)?:0+)?\.0+$/;
+var _TRIM_TIMEZONE = /(T[0-9:.]+)$/;
+
+var Date2 = _createObject2(
+ Date,
+ function(yy, mm, dd, h, m, s, ms) {
+ switch (arguments.length) {
+ case 0: return new Date;
+ case 1: return typeof yy == "number" ? new Date(yy) : Date2.parse(yy);
+ default: return new Date(yy, mm, arguments.length == 2 ? 1 : dd, h || 0, m || 0, s || 0, ms || 0);
+ }
+ }, "", {
+ toISOString: function(date) {
+ var string = "####-##-##T##:##:##.###";
+ for (var part in _DATE_PARTS) {
+ string = string.replace(/#+/, function(digits) {
+ var value = date["getUTC" + part]();
+ if (part == "Month") value++; // js month starts at zero
+ return ("000" + value).slice(-digits.length); // pad
+ });
+ }
+ // remove trailing zeroes, and remove UTC timezone, when time's absent
+ return string.replace(_TRIM_ZEROES, "").replace(_TRIM_TIMEZONE, "$1Z");
+ }
+ }
+);
+
+delete Date2.forEach;
+
+Date2.now = function() {
+ return (new Date).valueOf(); // milliseconds since the epoch
+};
+
+Date2.parse = function(string, defaultDate) {
+ if (arguments.length > 1) {
+ assertType(defaultDate, "number", "default date should be of type 'number'.")
+ }
+ // parse ISO date
+ var parts = match(string, _DATE_PATTERN);
+ if (parts.length) {
+ if (parts[_DATE_PARTS.Month]) parts[_DATE_PARTS.Month]--; // js months start at zero
+ // round milliseconds on 3 digits
+ if (parts[_TIMEZONE_PARTS.Hectomicroseconds] >= 5) parts[_DATE_PARTS.Milliseconds]++;
+ var date = new Date(defaultDate || 0);
+ var prefix = parts[_TIMEZONE_PARTS.UTC] || parts[_TIMEZONE_PARTS.Hours] ? "UTC" : "";
+ for (var part in _DATE_PARTS) {
+ var value = parts[_DATE_PARTS[part]];
+ if (!value) continue; // empty value
+ // set a date part
+ date["set" + prefix + part](value);
+ // make sure that this setting does not overflow
+ if (date["get" + prefix + part]() != parts[_DATE_PARTS[part]]) {
+ return NaN;
+ }
+ }
+ // timezone can be set, without time being available
+ // without a timezone, local timezone is respected
+ if (parts[_TIMEZONE_PARTS.Hours]) {
+ var hours = Number(parts[_TIMEZONE_PARTS.Sign] + parts[_TIMEZONE_PARTS.Hours]);
+ var minutes = Number(parts[_TIMEZONE_PARTS.Sign] + (parts[_TIMEZONE_PARTS.Minutes] || 0));
+ date.setUTCMinutes(date.getUTCMinutes() + (hours * 60) + minutes);
+ }
+ return date.valueOf();
+ } else {
+ return Date.parse(string);
+ }
+};
+
+// =========================================================================
+// JavaScript/String2.js
+// =========================================================================
+
+var String2 = _createObject2(
+ String,
+ function(string) {
+ return new String(arguments.length == 0 ? "" : string);
+ },
+ "charAt,charCodeAt,concat,indexOf,lastIndexOf,match,replace,search,slice,split,substr,substring,toLowerCase,toUpperCase",
+ {
+ csv: csv,
+ format: format,
+ rescape: rescape,
+ trim: trim
+ }
+);
+
+delete String2.forEach;
+
+// http://blog.stevenlevithan.com/archives/faster-trim-javascript
+function trim(string) {
+ return String(string).replace(_LTRIM, "").replace(_RTRIM, "");
+};
+
+function csv(string) {
+ return string ? (string + "").split(/\s*,\s*/) : [];
+};
+
+function format(string) {
+ // Replace %n with arguments[n].
+ // e.g. format("%1 %2%3 %2a %1%3", "she", "se", "lls");
+ // ==> "she sells sea shells"
+ // Only %1 - %9 supported.
+ var args = arguments;
+ var pattern = new RegExp("%([1-" + (arguments.length - 1) + "])", "g");
+ return (string + "").replace(pattern, function(match, index) {
+ return args[index];
+ });
+};
+
+function match(string, expression) {
+ // Same as String.match() except that this function will return an empty
+ // array if there is no match.
+ return (string + "").match(expression) || [];
+};
+
+function rescape(string) {
+ // Make a string safe for creating a RegExp.
+ return (string + "").replace(_RESCAPE, "\\$1");
+};
+
+// =========================================================================
+// JavaScript/Function2.js
+// =========================================================================
+
+var Function2 = _createObject2(
+ Function,
+ Function,
+ "", {
+ I: I,
+ II: II,
+ K: K,
+ bind: bind,
+ compose: compose,
+ delegate: delegate,
+ flip: flip,
+ not: not,
+ partial: partial,
+ unbind: unbind
+ }
+);
+
+function I(i) { // return first argument
+ return i;
+};
+
+function II(i, ii) { // return second argument
+ return ii;
+};
+
+function K(k) {
+ return function() {
+ return k;
+ };
+};
+
+function bind(fn, context) {
+ var lateBound = typeof fn != "function";
+ if (arguments.length > 2) {
+ var args = _slice.call(arguments, 2);
+ return function() {
+ return (lateBound ? context[fn] : fn).apply(context, args.concat.apply(args, arguments));
+ };
+ } else { // faster if there are no additional arguments
+ return function() {
+ return (lateBound ? context[fn] : fn).apply(context, arguments);
+ };
+ }
+};
+
+function compose() {
+ var fns = _slice.call(arguments);
+ return function() {
+ var i = fns.length, result = fns[--i].apply(this, arguments);
+ while (i--) result = fns[i].call(this, result);
+ return result;
+ };
+};
+
+function delegate(fn, context) {
+ return function() {
+ var args = _slice.call(arguments);
+ args.unshift(this);
+ return fn.apply(context, args);
+ };
+};
+
+function flip(fn) {
+ return function() {
+ return fn.apply(this, Array2.swap(arguments, 0, 1));
+ };
+};
+
+function not(fn) {
+ return function() {
+ return !fn.apply(this, arguments);
+ };
+};
+
+function partial(fn) {
+ var args = _slice.call(arguments, 1);
+ // based on Oliver Steele's version
+ return function() {
+ var specialised = args.concat(), i = 0, j = 0;
+ while (i < args.length && j < arguments.length) {
+ if (specialised[i] === undefined) specialised[i] = arguments[j++];
+ i++;
+ }
+ while (j < arguments.length) {
+ specialised[i++] = arguments[j++];
+ }
+ if (Array2.contains(specialised, undefined)) {
+ specialised.unshift(fn);
+ return partial.apply(null, specialised);
+ }
+ return fn.apply(this, specialised);
+ };
+};
+
+function unbind(fn) {
+ return function(context) {
+ return fn.apply(context, _slice.call(arguments, 1));
+ };
+};
+
+// =========================================================================
+// base2/detect.js
+// =========================================================================
+
+function detect() {
+ // Two types of detection:
+ // 1. Object detection
+ // e.g. detect("(java)");
+ // e.g. detect("!(document.addEventListener)");
+ // 2. Platform detection (browser sniffing)
+ // e.g. detect("MSIE");
+ // e.g. detect("MSIE|opera");
+
+ var jscript = NaN/*@cc_on||@_jscript_version@*/; // http://dean.edwards.name/weblog/2007/03/sniff/#comment85164
+ var javaEnabled = global.java ? true : false;
+ if (global.navigator) { // browser
+ var MSIE = /MSIE[\d.]+/g;
+ var element = document.createElement("span");
+ // Close up the space between name and version number.
+ // e.g. MSIE 6 -> MSIE6
+ var userAgent = navigator.userAgent.replace(/([a-z])[\s\/](\d)/gi, "$1$2");
+ // Fix opera's (and others) user agent string.
+ if (!jscript) userAgent = userAgent.replace(MSIE, "");
+ if (MSIE.test(userAgent)) userAgent = userAgent.match(MSIE)[0] + " " + userAgent.replace(MSIE, "");
+ base2.userAgent = navigator.platform + " " + userAgent.replace(/like \w+/gi, "");
+ javaEnabled &= navigator.javaEnabled();
+//} else if (java) { // rhino
+// var System = java.lang.System;
+// base2.userAgent = "Rhino " + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version");
+//} else if (jscript) { // Windows Scripting Host
+// base2.userAgent = "WSH";
+ }
+
+ var _cache = {};
+ detect = function(expression) {
+ if (_cache[expression] == null) {
+ var returnValue = false, test = expression;
+ var not = test.charAt(0) == "!";
+ if (not) test = test.slice(1);
+ if (test.charAt(0) == "(") {
+ try {
+ returnValue = new Function("element,jscript,java,global", "return !!" + test)(element, jscript, javaEnabled, global);
+ } catch (ex) {
+ // the test failed
+ }
+ } else {
+ // Browser sniffing.
+ returnValue = new RegExp("(" + test + ")", "i").test(base2.userAgent);
+ }
+ _cache[expression] = !!(not ^ returnValue);
+ }
+ return _cache[expression];
+ };
+
+ return detect(arguments[0]);
+};
+
+// =========================================================================
+// base2/init.js
+// =========================================================================
+
+base2 = global.base2 = new Package(this, base2);
+var exports = this.exports;
+
+lang = new Package(this, lang);
+exports += this.exports;
+
+JavaScript = new Package(this, JavaScript);
+eval(exports + this.exports);
+
+lang.base = base;
+lang.extend = extend;
+
+}; //////////////////// END: CLOSURE /////////////////////////////////////
diff --git a/src/umbraco.controls/FolderBrowser/FolderBrowser.cs b/src/umbraco.controls/FolderBrowser/FolderBrowser.cs
new file mode 100644
index 0000000000..0e0d190f74
--- /dev/null
+++ b/src/umbraco.controls/FolderBrowser/FolderBrowser.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Web;
+using System.Web.UI;
+using System.Web.UI.WebControls;
+using ClientDependency.Core;
+using umbraco.BasePages;
+using umbraco.cms.businesslogic.media;
+
+namespace umbraco.uicontrols.FolderBrowser
+{
+ [ClientDependency(ClientDependencyType.Css, "FolderBrowser/css/folderbrowser.css", "UmbracoClient")]
+ [ClientDependency(ClientDependencyType.Javascript, "ui/jquery.js", "UmbracoClient")]
+ [ClientDependency(ClientDependencyType.Javascript, "ui/base2.js", "UmbracoClient")]
+ [ClientDependency(ClientDependencyType.Javascript, "ui/knockout.js", "UmbracoClient")]
+ [ClientDependency(ClientDependencyType.Javascript, "FolderBrowser/js/folderbrowser.js", "UmbracoClient")]
+ [ToolboxData("<{0}:FolderBrowser runat=server>{0}:FolderBrowser>")]
+ public class FolderBrowser : WebControl
+ {
+ protected Panel panel;
+
+ protected int ParentId
+ {
+ get
+ {
+ // Try and parse from querystring
+ if(!string.IsNullOrEmpty(Context.Request.QueryString["id"]))
+ {
+ int id;
+ if(Int32.TryParse(Context.Request.QueryString["id"], out id))
+ return id;
+ }
+
+ // Get users root media folder id
+ var currentUser = UmbracoEnsuredPage.CurrentUser;
+ if (currentUser != null)
+ return currentUser.StartMediaId;
+
+ // Nothing else to check so just return -1
+ return -1;
+ }
+ }
+
+ protected Media ParentNode
+ {
+ get
+ {
+ return new Media(ParentId);
+ }
+ }
+
+ protected override void OnInit(EventArgs e)
+ {
+ base.OnInit(e);
+
+ EnsureChildControls();
+
+ //disable view state for this control
+ this.EnableViewState = false;
+ }
+
+ ///
+ /// Create the native .net child controls for this control
+ ///
+ protected override void CreateChildControls()
+ {
+ // Create the panel surround
+ panel = new Panel
+ {
+ ID = "FolderBrowser",
+ CssClass = "umbFolderBrowser"
+ };
+
+ panel.Attributes.Add("data-parentid", ParentId.ToString());
+
+ var sb = new StringBuilder();
+
+ // Create the breadcrumb
+ var breadCrumb = new List();
+ breadCrumb.Add(ParentNode);
+
+ var parent = ParentNode;
+ while(parent.Id != -1)
+ {
+ parent = new Media(parent.ParentId);
+ breadCrumb.Add(parent);
+ }
+
+ breadCrumb.Reverse();
+
+ sb.Append("- You are here:
");
+ foreach (var media in breadCrumb)
+ {
+ if(media.Id == ParentId)
+ if (media.Id == -1)
+ sb.AppendFormat("- Media
");
+ else
+ sb.AppendFormat("- {0}
", media.Text);
+ else
+ if (media.Id == -1)
+ sb.AppendFormat("- Media
");
+ else
+ sb.AppendFormat("- {0}
", media.Text, media.Id);
+ }
+ sb.Append("
");
+
+ // Create thumbnails container
+ sb.Append("" +
+ "![]()
" +
+ "
");
+
+ panel.Controls.Add(new LiteralControl(sb.ToString()));
+
+ Controls.Add(panel);
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ this.RenderContents(writer);
+ }
+ }
+}
diff --git a/src/umbraco.controls/umbraco.controls.csproj b/src/umbraco.controls/umbraco.controls.csproj
index 23c677eaf5..5cf9415626 100644
--- a/src/umbraco.controls/umbraco.controls.csproj
+++ b/src/umbraco.controls/umbraco.controls.csproj
@@ -86,6 +86,7 @@
Properties\SolutionInfo.cs
+