Browse Source

AMBARI-14583. Import visualsearch.js library to ambari-web (rzang)

Richard Zang 9 years ago
parent
commit
c7476d8a34

+ 124 - 0
LICENSE.txt

@@ -476,6 +476,70 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
 
 
+For jQuery UI Autocomplete 1.8.23 (ambari-web/vendor/scripts/jquery.ui.autocomplete.js):
+
+jQuery UI Autocomplete 1.8.23
+
+Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+Dual licensed under the MIT or GPL Version 2 licenses.
+http://jquery.org/license
+
+http://docs.jquery.com/UI/Autocomplete
+
+MIT license selected:
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+For jQuery UI Position 1.8.23 (ambari-web/vendor/scripts/jquery.ui.position.js):
+
+jQuery UI Position 1.8.23
+
+Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+Dual licensed under the MIT or GPL Version 2 licenses.
+http://jquery.org/license
+
+http://docs.jquery.com/UI/Position
+
+MIT license selected:
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
 For jQuery UI Datepicker 1.8.23 (ambari-web/vendor/scripts/jquery.ui.datepicker.js):
 For jQuery UI Datepicker 1.8.23 (ambari-web/vendor/scripts/jquery.ui.datepicker.js):
 
 
 jQuery UI Datepicker 1.8.23
 jQuery UI Datepicker 1.8.23
@@ -614,6 +678,66 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
 
 
+For Backbone.js 1.1.0 (ambari/ambari-web/vendor/scripts/backbone.js,
+
+Backbone.js 1.1.0
+(c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
+(c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+Backbone may be freely distributed under the MIT license.
+For all details and documentation:
+http://backbonejs.org
+
+MIT license selected:
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+For Underscore.js 1.5.2 (ambari/ambari-web/vendor/scripts/underscore.js,
+
+Underscore.js 1.5.2
+http://underscorejs.org
+(c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+Underscore may be freely distributed under the MIT license.
+
+MIT license selected:
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
 For MockJax 1.6.0 (contrib/views/slider/src/main/resources/ui/app/assets/javascripts/jquery.mockjax.js,
 For MockJax 1.6.0 (contrib/views/slider/src/main/resources/ui/app/assets/javascripts/jquery.mockjax.js,
 /contrib/views/files/src/main/resources/ui/app/assets/javascripts/jquery.mockjax.js,
 /contrib/views/files/src/main/resources/ui/app/assets/javascripts/jquery.mockjax.js,
 /contrib/views/pig/src/main/resources/ui/pig-web/app/assets/static/javascripts/jquery.mockjax.js):
 /contrib/views/pig/src/main/resources/ui/pig-web/app/assets/static/javascripts/jquery.mockjax.js):

+ 2 - 1
ambari-web/app/config.js

@@ -78,7 +78,8 @@ App.supports = {
   showPageLoadTime: false,
   showPageLoadTime: false,
   skipComponentStartAfterInstall: false,
   skipComponentStartAfterInstall: false,
   storeKDCCredentials: true,
   storeKDCCredentials: true,
-  preInstallChecks: false
+  preInstallChecks: false,
+  hostComboSearchBox: false
 };
 };
 
 
 if (App.enableExperimental) {
 if (App.enableExperimental) {

+ 1 - 1
ambari-web/app/templates/main/host.hbs

@@ -54,7 +54,7 @@
       {{/view}}
       {{/view}}
     </div>
     </div>
   </div>
   </div>
-
+  {{view App.MainHostComboSearchBoxView}}
   <table class="table advanced-header-table table-bordered table-striped" id="hosts-table">
   <table class="table advanced-header-table table-bordered table-striped" id="hosts-table">
     <thead>
     <thead>
       {{#view view.sortView classNames="label-row" contentBinding="view.filteredContent"}}
       {{#view view.sortView classNames="label-row" contentBinding="view.filteredContent"}}

+ 22 - 0
ambari-web/app/templates/main/host/combo_search_box.hbs

@@ -0,0 +1,22 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+{{#if App.supports.hostComboSearchBox}}
+<br/>
+<div id="combo_search_box"></div>
+<div id="search_query">&nbsp;</div>
+{{/if}}

+ 1 - 0
ambari-web/app/views.js

@@ -109,6 +109,7 @@ require('views/main/alerts/manage_alert_notifications_view');
 require('views/main/charts');
 require('views/main/charts');
 require('views/main/views/details');
 require('views/main/views/details');
 require('views/main/host');
 require('views/main/host');
+require('views/main/host/combo_search_box');
 require('views/main/host/hosts_table_menu_view');
 require('views/main/host/hosts_table_menu_view');
 require('views/main/host/details');
 require('views/main/host/details');
 require('views/main/host/details/host_component_view');
 require('views/main/host/details/host_component_view');

+ 135 - 0
ambari-web/app/views/main/host/combo_search_box.js

@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+App.MainHostComboSearchBoxView = Em.View.extend({
+  templateName: require('templates/main/host/combo_search_box'),
+  didInsertElement: function () {
+    window.visualSearch = VS.init({
+      container: $('#combo_search_box'),
+      query: '',
+      showFacets: true,
+      unquotable: [
+        'text'
+      ],
+      callbacks: {
+        search: function (query, searchCollection) {
+          var $query = $('#search_query');
+          var count = searchCollection.size();
+          $query.stop().animate({opacity: 1}, {duration: 300, queue: false});
+          $query.html('<span class="raquo">&raquo;</span> You searched for: ' +
+          '<b>' + (query || '<i>nothing</i>') + '</b>. ' +
+          '(' + count + ' facet' + (count == 1 ? '' : 's') + ')');
+          clearTimeout(window.queryHideDelay);
+          window.queryHideDelay = setTimeout(function () {
+            $query.animate({
+              opacity: 0
+            }, {
+              duration: 1000,
+              queue: false
+            });
+          }, 2000);
+        },
+        facetMatches: function (callback) {
+          console.log('called');
+          callback([
+            {label: 'name', category: 'Host'},
+            {label: 'ip', category: 'Host'},
+            {label: 'version', category: 'Host'},
+            {label: 'health', category: 'Host'},
+            {label: 'service', category: 'Service'},
+            {label: 'component', category: 'Service'},
+            {label: 'state', category: 'Service'}
+          ]);
+        },
+        valueMatches: function (facet, searchTerm, callback) {
+          switch (facet) {
+            case 'name':
+              callback([
+                {value: 'c6401.ambari.apache.org', label: 'c6401.ambari.apache.org'},
+                {value: 'c6402.ambari.apache.org', label: 'c6402.ambari.apache.org'},
+                {value: 'c6403.ambari.apache.org', label: 'c6403.ambari.apache.org'}
+              ]);
+              break;
+            case 'ip':
+              callback(['192.168.64.101', '192.168.64.102', '192.168.64.103']);
+              break;
+            case 'rack':
+              callback(['/default-rack', '/default-rack-1', '/default-rack-2']);
+              break;
+            case 'version':
+              callback([
+                'HDP-2.0.0.0-1587',
+                'HDP-2.1.2.2-1576',
+                'HDP-2.2.4.0-2252',
+                'HDP-2.3.4.0-3485'
+              ]);
+              break;
+            case 'health':
+              callback([
+                'Healthy',
+                'Master Down',
+                'Slave Down',
+                'Lost Heartbeat',
+                'Alerts',
+                'Restart',
+                'Maintainance Mode'
+              ]);
+              break;
+            case 'service':
+              callback([
+                'HDFS',
+                'YARN',
+                'HIVE',
+                'HBASE',
+                'Storm',
+                'Oozie',
+                'Falcon',
+                'Pig',
+                'Spark',
+                'Zookeeper',
+                'AMS',
+                'Ranger'
+              ]);
+              break;
+            case 'component':
+              callback([
+                'NameNode',
+                'SNameNode',
+                'ZooKeeper Server',
+                'DataNode',
+                'HDFS Client',
+                'Zookeeper Client'
+              ], {preserveOrder: true});
+              break;
+            case 'state':
+              callback([
+                'Started',
+                'Stopped',
+                'Install Failed',
+                'Decommissioning',
+                'Decommissioned'
+              ], {preserveOrder: true});
+              break;
+          }
+        }
+      }
+    });
+  }
+});

+ 8 - 1
ambari-web/config.coffee

@@ -45,7 +45,9 @@ exports.config =
           'vendor/scripts/d3.v2.js',
           'vendor/scripts/d3.v2.js',
           'vendor/scripts/cubism.v1.js',
           'vendor/scripts/cubism.v1.js',
           'vendor/scripts/jquery.ui.core.js',
           'vendor/scripts/jquery.ui.core.js',
+          'vendor/scripts/jquery.ui.position.js',
           'vendor/scripts/jquery.ui.widget.js',
           'vendor/scripts/jquery.ui.widget.js',
+          'vendor/scripts/jquery.ui.autocomplete.js',
           'vendor/scripts/jquery.ui.mouse.js',
           'vendor/scripts/jquery.ui.mouse.js',
           'vendor/scripts/jquery.ui.datepicker.js',
           'vendor/scripts/jquery.ui.datepicker.js',
           'vendor/scripts/jquery-ui-timepicker-addon.js',
           'vendor/scripts/jquery-ui-timepicker-addon.js',
@@ -55,6 +57,9 @@ exports.config =
           'vendor/scripts/jquery.timeago.js',
           'vendor/scripts/jquery.timeago.js',
           'vendor/scripts/jquery.ajax-retry.js',
           'vendor/scripts/jquery.ajax-retry.js',
           'vendor/scripts/jquery.sticky-kit.js',
           'vendor/scripts/jquery.sticky-kit.js',
+          'vendor/scripts/underscore.js',
+          'vendor/scripts/backbone.js',
+          'vendor/scripts/visualsearch.js',
           'vendor/scripts/moment.js',
           'vendor/scripts/moment.js',
           'vendor/scripts/moment-timezone-with-data-2010-2020.js',
           'vendor/scripts/moment-timezone-with-data-2010-2020.js',
           'vendor/scripts/workflow_visualization.js',
           'vendor/scripts/workflow_visualization.js',
@@ -63,6 +68,7 @@ exports.config =
           'vendor/scripts/jquery.flexibleArea.js',
           'vendor/scripts/jquery.flexibleArea.js',
           'vendor/scripts/FileSaver.js',
           'vendor/scripts/FileSaver.js',
           'vendor/scripts/Blob.js'
           'vendor/scripts/Blob.js'
+
           ]
           ]
 
 
     stylesheets:
     stylesheets:
@@ -79,7 +85,8 @@ exports.config =
           'vendor/styles/rickshaw.css',
           'vendor/styles/rickshaw.css',
           'vendor/styles/bootstrap-combobox.css',
           'vendor/styles/bootstrap-combobox.css',
           'vendor/styles/bootstrap-checkbox.css',
           'vendor/styles/bootstrap-checkbox.css',
-          'vendor/styles/bootstrap-slider.min.css'
+          'vendor/styles/bootstrap-slider.min.css',
+          'vendor/styles/visualsearch-datauri.css'
         ],
         ],
         after: ['app/styles/custom-ui.css']
         after: ['app/styles/custom-ui.css']
 
 

+ 5 - 0
ambari-web/karma.conf.js

@@ -57,7 +57,9 @@ module.exports = function(config) {
       'vendor/scripts/d3.v2.js',
       'vendor/scripts/d3.v2.js',
       'vendor/scripts/cubism.v1.js',
       'vendor/scripts/cubism.v1.js',
       'vendor/scripts/jquery.ui.core.js',
       'vendor/scripts/jquery.ui.core.js',
+      'vendor/scripts/jquery.ui.position.js',
       'vendor/scripts/jquery.ui.widget.js',
       'vendor/scripts/jquery.ui.widget.js',
+      'vendor/scripts/jquery.ui.autocomplete.js',
       'vendor/scripts/jquery.ui.mouse.js',
       'vendor/scripts/jquery.ui.mouse.js',
       'vendor/scripts/jquery.ui.datepicker.js',
       'vendor/scripts/jquery.ui.datepicker.js',
       'vendor/scripts/jquery-ui-timepicker-addon.js',
       'vendor/scripts/jquery-ui-timepicker-addon.js',
@@ -66,6 +68,9 @@ module.exports = function(config) {
       'vendor/scripts/jquery.ui.custom-effects.js',
       'vendor/scripts/jquery.ui.custom-effects.js',
       'vendor/scripts/jquery.timeago.js',
       'vendor/scripts/jquery.timeago.js',
       'vendor/scripts/jquery.ajax-retry.js',
       'vendor/scripts/jquery.ajax-retry.js',
+      'vendor/scripts/underscore.js',
+      'vendor/scripts/backbone.js',
+      'vendor/scripts/visualsearch.js',
       'vendor/scripts/workflow_visualization.js',
       'vendor/scripts/workflow_visualization.js',
       'vendor/scripts/rickshaw.js',
       'vendor/scripts/rickshaw.js',
       'vendor/scripts/spin.js',
       'vendor/scripts/spin.js',

+ 1581 - 0
ambari-web/vendor/scripts/backbone.js

@@ -0,0 +1,1581 @@
+//     Backbone.js 1.1.0
+
+//     (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
+//     (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Backbone may be freely distributed under the MIT license.
+//     For all details and documentation:
+//     http://backbonejs.org
+
+(function(){
+
+    // Initial Setup
+    // -------------
+
+    // Save a reference to the global object (`window` in the browser, `exports`
+    // on the server).
+    var root = this;
+
+    // Save the previous value of the `Backbone` variable, so that it can be
+    // restored later on, if `noConflict` is used.
+    var previousBackbone = root.Backbone;
+
+    // Create local references to array methods we'll want to use later.
+    var array = [];
+    var push = array.push;
+    var slice = array.slice;
+    var splice = array.splice;
+
+    // The top-level namespace. All public Backbone classes and modules will
+    // be attached to this. Exported for both the browser and the server.
+    var Backbone;
+    if (typeof exports !== 'undefined') {
+        Backbone = exports;
+    } else {
+        Backbone = root.Backbone = {};
+    }
+
+    // Current version of the library. Keep in sync with `package.json`.
+    Backbone.VERSION = '1.1.0';
+
+    // Require Underscore, if we're on the server, and it's not already present.
+    var _ = root._;
+    if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
+
+    // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
+    // the `$` variable.
+    Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
+
+    // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+    // to its previous owner. Returns a reference to this Backbone object.
+    Backbone.noConflict = function() {
+        root.Backbone = previousBackbone;
+        return this;
+    };
+
+    // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+    // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+    // set a `X-Http-Method-Override` header.
+    Backbone.emulateHTTP = false;
+
+    // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+    // `application/json` requests ... will encode the body as
+    // `application/x-www-form-urlencoded` instead and will send the model in a
+    // form param named `model`.
+    Backbone.emulateJSON = false;
+
+    // Backbone.Events
+    // ---------------
+
+    // A module that can be mixed in to *any object* in order to provide it with
+    // custom events. You may bind with `on` or remove with `off` callback
+    // functions to an event; `trigger`-ing an event fires all callbacks in
+    // succession.
+    //
+    //     var object = {};
+    //     _.extend(object, Backbone.Events);
+    //     object.on('expand', function(){ alert('expanded'); });
+    //     object.trigger('expand');
+    //
+    var Events = Backbone.Events = {
+
+        // Bind an event to a `callback` function. Passing `"all"` will bind
+        // the callback to all events fired.
+        on: function(name, callback, context) {
+            if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
+            this._events || (this._events = {});
+            var events = this._events[name] || (this._events[name] = []);
+            events.push({callback: callback, context: context, ctx: context || this});
+            return this;
+        },
+
+        // Bind an event to only be triggered a single time. After the first time
+        // the callback is invoked, it will be removed.
+        once: function(name, callback, context) {
+            if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
+            var self = this;
+            var once = _.once(function() {
+                self.off(name, once);
+                callback.apply(this, arguments);
+            });
+            once._callback = callback;
+            return this.on(name, once, context);
+        },
+
+        // Remove one or many callbacks. If `context` is null, removes all
+        // callbacks with that function. If `callback` is null, removes all
+        // callbacks for the event. If `name` is null, removes all bound
+        // callbacks for all events.
+        off: function(name, callback, context) {
+            var retain, ev, events, names, i, l, j, k;
+            if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
+            if (!name && !callback && !context) {
+                this._events = {};
+                return this;
+            }
+            names = name ? [name] : _.keys(this._events);
+            for (i = 0, l = names.length; i < l; i++) {
+                name = names[i];
+                if (events = this._events[name]) {
+                    this._events[name] = retain = [];
+                    if (callback || context) {
+                        for (j = 0, k = events.length; j < k; j++) {
+                            ev = events[j];
+                            if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
+                                (context && context !== ev.context)) {
+                                retain.push(ev);
+                            }
+                        }
+                    }
+                    if (!retain.length) delete this._events[name];
+                }
+            }
+
+            return this;
+        },
+
+        // Trigger one or many events, firing all bound callbacks. Callbacks are
+        // passed the same arguments as `trigger` is, apart from the event name
+        // (unless you're listening on `"all"`, which will cause your callback to
+        // receive the true name of the event as the first argument).
+        trigger: function(name) {
+            if (!this._events) return this;
+            var args = slice.call(arguments, 1);
+            if (!eventsApi(this, 'trigger', name, args)) return this;
+            var events = this._events[name];
+            var allEvents = this._events.all;
+            if (events) triggerEvents(events, args);
+            if (allEvents) triggerEvents(allEvents, arguments);
+            return this;
+        },
+
+        // Tell this object to stop listening to either specific events ... or
+        // to every object it's currently listening to.
+        stopListening: function(obj, name, callback) {
+            var listeningTo = this._listeningTo;
+            if (!listeningTo) return this;
+            var remove = !name && !callback;
+            if (!callback && typeof name === 'object') callback = this;
+            if (obj) (listeningTo = {})[obj._listenId] = obj;
+            for (var id in listeningTo) {
+                obj = listeningTo[id];
+                obj.off(name, callback, this);
+                if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
+            }
+            return this;
+        }
+
+    };
+
+    // Regular expression used to split event strings.
+    var eventSplitter = /\s+/;
+
+    // Implement fancy features of the Events API such as multiple event
+    // names `"change blur"` and jQuery-style event maps `{change: action}`
+    // in terms of the existing API.
+    var eventsApi = function(obj, action, name, rest) {
+        if (!name) return true;
+
+        // Handle event maps.
+        if (typeof name === 'object') {
+            for (var key in name) {
+                obj[action].apply(obj, [key, name[key]].concat(rest));
+            }
+            return false;
+        }
+
+        // Handle space separated event names.
+        if (eventSplitter.test(name)) {
+            var names = name.split(eventSplitter);
+            for (var i = 0, l = names.length; i < l; i++) {
+                obj[action].apply(obj, [names[i]].concat(rest));
+            }
+            return false;
+        }
+
+        return true;
+    };
+
+    // A difficult-to-believe, but optimized internal dispatch function for
+    // triggering events. Tries to keep the usual cases speedy (most internal
+    // Backbone events have 3 arguments).
+    var triggerEvents = function(events, args) {
+        var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
+        switch (args.length) {
+            case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
+            case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
+            case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
+            case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
+            default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
+        }
+    };
+
+    var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
+
+    // Inversion-of-control versions of `on` and `once`. Tell *this* object to
+    // listen to an event in another object ... keeping track of what it's
+    // listening to.
+    _.each(listenMethods, function(implementation, method) {
+        Events[method] = function(obj, name, callback) {
+            var listeningTo = this._listeningTo || (this._listeningTo = {});
+            var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
+            listeningTo[id] = obj;
+            if (!callback && typeof name === 'object') callback = this;
+            obj[implementation](name, callback, this);
+            return this;
+        };
+    });
+
+    // Aliases for backwards compatibility.
+    Events.bind   = Events.on;
+    Events.unbind = Events.off;
+
+    // Allow the `Backbone` object to serve as a global event bus, for folks who
+    // want global "pubsub" in a convenient place.
+    _.extend(Backbone, Events);
+
+    // Backbone.Model
+    // --------------
+
+    // Backbone **Models** are the basic data object in the framework --
+    // frequently representing a row in a table in a database on your server.
+    // A discrete chunk of data and a bunch of useful, related methods for
+    // performing computations and transformations on that data.
+
+    // Create a new model with the specified attributes. A client id (`cid`)
+    // is automatically generated and assigned for you.
+    var Model = Backbone.Model = function(attributes, options) {
+        var attrs = attributes || {};
+        options || (options = {});
+        this.cid = _.uniqueId('c');
+        this.attributes = {};
+        if (options.collection) this.collection = options.collection;
+        if (options.parse) attrs = this.parse(attrs, options) || {};
+        attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
+        this.set(attrs, options);
+        this.changed = {};
+        this.initialize.apply(this, arguments);
+    };
+
+    // Attach all inheritable methods to the Model prototype.
+    _.extend(Model.prototype, Events, {
+
+        // A hash of attributes whose current and previous value differ.
+        changed: null,
+
+        // The value returned during the last failed validation.
+        validationError: null,
+
+        // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+        // CouchDB users may want to set this to `"_id"`.
+        idAttribute: 'id',
+
+        // Initialize is an empty function by default. Override it with your own
+        // initialization logic.
+        initialize: function(){},
+
+        // Return a copy of the model's `attributes` object.
+        toJSON: function(options) {
+            return _.clone(this.attributes);
+        },
+
+        // Proxy `Backbone.sync` by default -- but override this if you need
+        // custom syncing semantics for *this* particular model.
+        sync: function() {
+            return Backbone.sync.apply(this, arguments);
+        },
+
+        // Get the value of an attribute.
+        get: function(attr) {
+            return this.attributes[attr];
+        },
+
+        // Get the HTML-escaped value of an attribute.
+        escape: function(attr) {
+            return _.escape(this.get(attr));
+        },
+
+        // Returns `true` if the attribute contains a value that is not null
+        // or undefined.
+        has: function(attr) {
+            return this.get(attr) != null;
+        },
+
+        // Set a hash of model attributes on the object, firing `"change"`. This is
+        // the core primitive operation of a model, updating the data and notifying
+        // anyone who needs to know about the change in state. The heart of the beast.
+        set: function(key, val, options) {
+            var attr, attrs, unset, changes, silent, changing, prev, current;
+            if (key == null) return this;
+
+            // Handle both `"key", value` and `{key: value}` -style arguments.
+            if (typeof key === 'object') {
+                attrs = key;
+                options = val;
+            } else {
+                (attrs = {})[key] = val;
+            }
+
+            options || (options = {});
+
+            // Run validation.
+            if (!this._validate(attrs, options)) return false;
+
+            // Extract attributes and options.
+            unset           = options.unset;
+            silent          = options.silent;
+            changes         = [];
+            changing        = this._changing;
+            this._changing  = true;
+
+            if (!changing) {
+                this._previousAttributes = _.clone(this.attributes);
+                this.changed = {};
+            }
+            current = this.attributes, prev = this._previousAttributes;
+
+            // Check for changes of `id`.
+            if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+            // For each `set` attribute, update or delete the current value.
+            for (attr in attrs) {
+                val = attrs[attr];
+                if (!_.isEqual(current[attr], val)) changes.push(attr);
+                if (!_.isEqual(prev[attr], val)) {
+                    this.changed[attr] = val;
+                } else {
+                    delete this.changed[attr];
+                }
+                unset ? delete current[attr] : current[attr] = val;
+            }
+
+            // Trigger all relevant attribute changes.
+            if (!silent) {
+                if (changes.length) this._pending = true;
+                for (var i = 0, l = changes.length; i < l; i++) {
+                    this.trigger('change:' + changes[i], this, current[changes[i]], options);
+                }
+            }
+
+            // You might be wondering why there's a `while` loop here. Changes can
+            // be recursively nested within `"change"` events.
+            if (changing) return this;
+            if (!silent) {
+                while (this._pending) {
+                    this._pending = false;
+                    this.trigger('change', this, options);
+                }
+            }
+            this._pending = false;
+            this._changing = false;
+            return this;
+        },
+
+        // Remove an attribute from the model, firing `"change"`. `unset` is a noop
+        // if the attribute doesn't exist.
+        unset: function(attr, options) {
+            return this.set(attr, void 0, _.extend({}, options, {unset: true}));
+        },
+
+        // Clear all attributes on the model, firing `"change"`.
+        clear: function(options) {
+            var attrs = {};
+            for (var key in this.attributes) attrs[key] = void 0;
+            return this.set(attrs, _.extend({}, options, {unset: true}));
+        },
+
+        // Determine if the model has changed since the last `"change"` event.
+        // If you specify an attribute name, determine if that attribute has changed.
+        hasChanged: function(attr) {
+            if (attr == null) return !_.isEmpty(this.changed);
+            return _.has(this.changed, attr);
+        },
+
+        // Return an object containing all the attributes that have changed, or
+        // false if there are no changed attributes. Useful for determining what
+        // parts of a view need to be updated and/or what attributes need to be
+        // persisted to the server. Unset attributes will be set to undefined.
+        // You can also pass an attributes object to diff against the model,
+        // determining if there *would be* a change.
+        changedAttributes: function(diff) {
+            if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+            var val, changed = false;
+            var old = this._changing ? this._previousAttributes : this.attributes;
+            for (var attr in diff) {
+                if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+                (changed || (changed = {}))[attr] = val;
+            }
+            return changed;
+        },
+
+        // Get the previous value of an attribute, recorded at the time the last
+        // `"change"` event was fired.
+        previous: function(attr) {
+            if (attr == null || !this._previousAttributes) return null;
+            return this._previousAttributes[attr];
+        },
+
+        // Get all of the attributes of the model at the time of the previous
+        // `"change"` event.
+        previousAttributes: function() {
+            return _.clone(this._previousAttributes);
+        },
+
+        // Fetch the model from the server. If the server's representation of the
+        // model differs from its current attributes, they will be overridden,
+        // triggering a `"change"` event.
+        fetch: function(options) {
+            options = options ? _.clone(options) : {};
+            if (options.parse === void 0) options.parse = true;
+            var model = this;
+            var success = options.success;
+            options.success = function(resp) {
+                if (!model.set(model.parse(resp, options), options)) return false;
+                if (success) success(model, resp, options);
+                model.trigger('sync', model, resp, options);
+            };
+            wrapError(this, options);
+            return this.sync('read', this, options);
+        },
+
+        // Set a hash of model attributes, and sync the model to the server.
+        // If the server returns an attributes hash that differs, the model's
+        // state will be `set` again.
+        save: function(key, val, options) {
+            var attrs, method, xhr, attributes = this.attributes;
+
+            // Handle both `"key", value` and `{key: value}` -style arguments.
+            if (key == null || typeof key === 'object') {
+                attrs = key;
+                options = val;
+            } else {
+                (attrs = {})[key] = val;
+            }
+
+            options = _.extend({validate: true}, options);
+
+            // If we're not waiting and attributes exist, save acts as
+            // `set(attr).save(null, opts)` with validation. Otherwise, check if
+            // the model will be valid when the attributes, if any, are set.
+            if (attrs && !options.wait) {
+                if (!this.set(attrs, options)) return false;
+            } else {
+                if (!this._validate(attrs, options)) return false;
+            }
+
+            // Set temporary attributes if `{wait: true}`.
+            if (attrs && options.wait) {
+                this.attributes = _.extend({}, attributes, attrs);
+            }
+
+            // After a successful server-side save, the client is (optionally)
+            // updated with the server-side state.
+            if (options.parse === void 0) options.parse = true;
+            var model = this;
+            var success = options.success;
+            options.success = function(resp) {
+                // Ensure attributes are restored during synchronous saves.
+                model.attributes = attributes;
+                var serverAttrs = model.parse(resp, options);
+                if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
+                if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
+                    return false;
+                }
+                if (success) success(model, resp, options);
+                model.trigger('sync', model, resp, options);
+            };
+            wrapError(this, options);
+
+            method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
+            if (method === 'patch') options.attrs = attrs;
+            xhr = this.sync(method, this, options);
+
+            // Restore attributes.
+            if (attrs && options.wait) this.attributes = attributes;
+
+            return xhr;
+        },
+
+        // Destroy this model on the server if it was already persisted.
+        // Optimistically removes the model from its collection, if it has one.
+        // If `wait: true` is passed, waits for the server to respond before removal.
+        destroy: function(options) {
+            options = options ? _.clone(options) : {};
+            var model = this;
+            var success = options.success;
+
+            var destroy = function() {
+                model.trigger('destroy', model, model.collection, options);
+            };
+
+            options.success = function(resp) {
+                if (options.wait || model.isNew()) destroy();
+                if (success) success(model, resp, options);
+                if (!model.isNew()) model.trigger('sync', model, resp, options);
+            };
+
+            if (this.isNew()) {
+                options.success();
+                return false;
+            }
+            wrapError(this, options);
+
+            var xhr = this.sync('delete', this, options);
+            if (!options.wait) destroy();
+            return xhr;
+        },
+
+        // Default URL for the model's representation on the server -- if you're
+        // using Backbone's restful methods, override this to change the endpoint
+        // that will be called.
+        url: function() {
+            var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
+            if (this.isNew()) return base;
+            return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
+        },
+
+        // **parse** converts a response into the hash of attributes to be `set` on
+        // the model. The default implementation is just to pass the response along.
+        parse: function(resp, options) {
+            return resp;
+        },
+
+        // Create a new model with identical attributes to this one.
+        clone: function() {
+            return new this.constructor(this.attributes);
+        },
+
+        // A model is new if it has never been saved to the server, and lacks an id.
+        isNew: function() {
+            return this.id == null;
+        },
+
+        // Check if the model is currently in a valid state.
+        isValid: function(options) {
+            return this._validate({}, _.extend(options || {}, { validate: true }));
+        },
+
+        // Run validation against the next complete set of model attributes,
+        // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
+        _validate: function(attrs, options) {
+            if (!options.validate || !this.validate) return true;
+            attrs = _.extend({}, this.attributes, attrs);
+            var error = this.validationError = this.validate(attrs, options) || null;
+            if (!error) return true;
+            this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
+            return false;
+        }
+
+    });
+
+    // Underscore methods that we want to implement on the Model.
+    var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
+
+    // Mix in each Underscore method as a proxy to `Model#attributes`.
+    _.each(modelMethods, function(method) {
+        Model.prototype[method] = function() {
+            var args = slice.call(arguments);
+            args.unshift(this.attributes);
+            return _[method].apply(_, args);
+        };
+    });
+
+    // Backbone.Collection
+    // -------------------
+
+    // If models tend to represent a single row of data, a Backbone Collection is
+    // more analagous to a table full of data ... or a small slice or page of that
+    // table, or a collection of rows that belong together for a particular reason
+    // -- all of the messages in this particular folder, all of the documents
+    // belonging to this particular author, and so on. Collections maintain
+    // indexes of their models, both in order, and for lookup by `id`.
+
+    // Create a new **Collection**, perhaps to contain a specific type of `model`.
+    // If a `comparator` is specified, the Collection will maintain
+    // its models in sort order, as they're added and removed.
+    var Collection = Backbone.Collection = function(models, options) {
+        options || (options = {});
+        if (options.model) this.model = options.model;
+        if (options.comparator !== void 0) this.comparator = options.comparator;
+        this._reset();
+        this.initialize.apply(this, arguments);
+        if (models) this.reset(models, _.extend({silent: true}, options));
+    };
+
+    // Default options for `Collection#set`.
+    var setOptions = {add: true, remove: true, merge: true};
+    var addOptions = {add: true, remove: false};
+
+    // Define the Collection's inheritable methods.
+    _.extend(Collection.prototype, Events, {
+
+        // The default model for a collection is just a **Backbone.Model**.
+        // This should be overridden in most cases.
+        model: Model,
+
+        // Initialize is an empty function by default. Override it with your own
+        // initialization logic.
+        initialize: function(){},
+
+        // The JSON representation of a Collection is an array of the
+        // models' attributes.
+        toJSON: function(options) {
+            return this.map(function(model){ return model.toJSON(options); });
+        },
+
+        // Proxy `Backbone.sync` by default.
+        sync: function() {
+            return Backbone.sync.apply(this, arguments);
+        },
+
+        // Add a model, or list of models to the set.
+        add: function(models, options) {
+            return this.set(models, _.extend({merge: false}, options, addOptions));
+        },
+
+        // Remove a model, or a list of models from the set.
+        remove: function(models, options) {
+            var singular = !_.isArray(models);
+            models = singular ? [models] : _.clone(models);
+            options || (options = {});
+            var i, l, index, model;
+            for (i = 0, l = models.length; i < l; i++) {
+                model = models[i] = this.get(models[i]);
+                if (!model) continue;
+                delete this._byId[model.id];
+                delete this._byId[model.cid];
+                index = this.indexOf(model);
+                this.models.splice(index, 1);
+                this.length--;
+                if (!options.silent) {
+                    options.index = index;
+                    model.trigger('remove', model, this, options);
+                }
+                this._removeReference(model);
+            }
+            return singular ? models[0] : models;
+        },
+
+        // Update a collection by `set`-ing a new list of models, adding new ones,
+        // removing models that are no longer present, and merging models that
+        // already exist in the collection, as necessary. Similar to **Model#set**,
+        // the core operation for updating the data contained by the collection.
+        set: function(models, options) {
+            options = _.defaults({}, options, setOptions);
+            if (options.parse) models = this.parse(models, options);
+            var singular = !_.isArray(models);
+            models = singular ? (models ? [models] : []) : _.clone(models);
+            var i, l, id, model, attrs, existing, sort;
+            var at = options.at;
+            var targetModel = this.model;
+            var sortable = this.comparator && (at == null) && options.sort !== false;
+            var sortAttr = _.isString(this.comparator) ? this.comparator : null;
+            var toAdd = [], toRemove = [], modelMap = {};
+            var add = options.add, merge = options.merge, remove = options.remove;
+            var order = !sortable && add && remove ? [] : false;
+
+            // Turn bare objects into model references, and prevent invalid models
+            // from being added.
+            for (i = 0, l = models.length; i < l; i++) {
+                attrs = models[i];
+                if (attrs instanceof Model) {
+                    id = model = attrs;
+                } else {
+                    id = attrs[targetModel.prototype.idAttribute];
+                }
+
+                // If a duplicate is found, prevent it from being added and
+                // optionally merge it into the existing model.
+                if (existing = this.get(id)) {
+                    if (remove) modelMap[existing.cid] = true;
+                    if (merge) {
+                        attrs = attrs === model ? model.attributes : attrs;
+                        if (options.parse) attrs = existing.parse(attrs, options);
+                        existing.set(attrs, options);
+                        if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
+                    }
+                    models[i] = existing;
+
+                    // If this is a new, valid model, push it to the `toAdd` list.
+                } else if (add) {
+                    model = models[i] = this._prepareModel(attrs, options);
+                    if (!model) continue;
+                    toAdd.push(model);
+
+                    // Listen to added models' events, and index models for lookup by
+                    // `id` and by `cid`.
+                    model.on('all', this._onModelEvent, this);
+                    this._byId[model.cid] = model;
+                    if (model.id != null) this._byId[model.id] = model;
+                }
+                if (order) order.push(existing || model);
+            }
+
+            // Remove nonexistent models if appropriate.
+            if (remove) {
+                for (i = 0, l = this.length; i < l; ++i) {
+                    if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
+                }
+                if (toRemove.length) this.remove(toRemove, options);
+            }
+
+            // See if sorting is needed, update `length` and splice in new models.
+            if (toAdd.length || (order && order.length)) {
+                if (sortable) sort = true;
+                this.length += toAdd.length;
+                if (at != null) {
+                    for (i = 0, l = toAdd.length; i < l; i++) {
+                        this.models.splice(at + i, 0, toAdd[i]);
+                    }
+                } else {
+                    if (order) this.models.length = 0;
+                    var orderedModels = order || toAdd;
+                    for (i = 0, l = orderedModels.length; i < l; i++) {
+                        this.models.push(orderedModels[i]);
+                    }
+                }
+            }
+
+            // Silently sort the collection if appropriate.
+            if (sort) this.sort({silent: true});
+
+            // Unless silenced, it's time to fire all appropriate add/sort events.
+            if (!options.silent) {
+                for (i = 0, l = toAdd.length; i < l; i++) {
+                    (model = toAdd[i]).trigger('add', model, this, options);
+                }
+                if (sort || (order && order.length)) this.trigger('sort', this, options);
+            }
+
+            // Return the added (or merged) model (or models).
+            return singular ? models[0] : models;
+        },
+
+        // When you have more items than you want to add or remove individually,
+        // you can reset the entire set with a new list of models, without firing
+        // any granular `add` or `remove` events. Fires `reset` when finished.
+        // Useful for bulk operations and optimizations.
+        reset: function(models, options) {
+            options || (options = {});
+            for (var i = 0, l = this.models.length; i < l; i++) {
+                this._removeReference(this.models[i]);
+            }
+            options.previousModels = this.models;
+            this._reset();
+            models = this.add(models, _.extend({silent: true}, options));
+            if (!options.silent) this.trigger('reset', this, options);
+            return models;
+        },
+
+        // Add a model to the end of the collection.
+        push: function(model, options) {
+            return this.add(model, _.extend({at: this.length}, options));
+        },
+
+        // Remove a model from the end of the collection.
+        pop: function(options) {
+            var model = this.at(this.length - 1);
+            this.remove(model, options);
+            return model;
+        },
+
+        // Add a model to the beginning of the collection.
+        unshift: function(model, options) {
+            return this.add(model, _.extend({at: 0}, options));
+        },
+
+        // Remove a model from the beginning of the collection.
+        shift: function(options) {
+            var model = this.at(0);
+            this.remove(model, options);
+            return model;
+        },
+
+        // Slice out a sub-array of models from the collection.
+        slice: function() {
+            return slice.apply(this.models, arguments);
+        },
+
+        // Get a model from the set by id.
+        get: function(obj) {
+            if (obj == null) return void 0;
+            return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj];
+        },
+
+        // Get the model at the given index.
+        at: function(index) {
+            return this.models[index];
+        },
+
+        // Return models with matching attributes. Useful for simple cases of
+        // `filter`.
+        where: function(attrs, first) {
+            if (_.isEmpty(attrs)) return first ? void 0 : [];
+            return this[first ? 'find' : 'filter'](function(model) {
+                for (var key in attrs) {
+                    if (attrs[key] !== model.get(key)) return false;
+                }
+                return true;
+            });
+        },
+
+        // Return the first model with matching attributes. Useful for simple cases
+        // of `find`.
+        findWhere: function(attrs) {
+            return this.where(attrs, true);
+        },
+
+        // Force the collection to re-sort itself. You don't need to call this under
+        // normal circumstances, as the set will maintain sort order as each item
+        // is added.
+        sort: function(options) {
+            if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+            options || (options = {});
+
+            // Run sort based on type of `comparator`.
+            if (_.isString(this.comparator) || this.comparator.length === 1) {
+                this.models = this.sortBy(this.comparator, this);
+            } else {
+                this.models.sort(_.bind(this.comparator, this));
+            }
+
+            if (!options.silent) this.trigger('sort', this, options);
+            return this;
+        },
+
+        // Pluck an attribute from each model in the collection.
+        pluck: function(attr) {
+            return _.invoke(this.models, 'get', attr);
+        },
+
+        // Fetch the default set of models for this collection, resetting the
+        // collection when they arrive. If `reset: true` is passed, the response
+        // data will be passed through the `reset` method instead of `set`.
+        fetch: function(options) {
+            options = options ? _.clone(options) : {};
+            if (options.parse === void 0) options.parse = true;
+            var success = options.success;
+            var collection = this;
+            options.success = function(resp) {
+                var method = options.reset ? 'reset' : 'set';
+                collection[method](resp, options);
+                if (success) success(collection, resp, options);
+                collection.trigger('sync', collection, resp, options);
+            };
+            wrapError(this, options);
+            return this.sync('read', this, options);
+        },
+
+        // Create a new instance of a model in this collection. Add the model to the
+        // collection immediately, unless `wait: true` is passed, in which case we
+        // wait for the server to agree.
+        create: function(model, options) {
+            options = options ? _.clone(options) : {};
+            if (!(model = this._prepareModel(model, options))) return false;
+            if (!options.wait) this.add(model, options);
+            var collection = this;
+            var success = options.success;
+            options.success = function(model, resp, options) {
+                if (options.wait) collection.add(model, options);
+                if (success) success(model, resp, options);
+            };
+            model.save(null, options);
+            return model;
+        },
+
+        // **parse** converts a response into a list of models to be added to the
+        // collection. The default implementation is just to pass it through.
+        parse: function(resp, options) {
+            return resp;
+        },
+
+        // Create a new collection with an identical list of models as this one.
+        clone: function() {
+            return new this.constructor(this.models);
+        },
+
+        // Private method to reset all internal state. Called when the collection
+        // is first initialized or reset.
+        _reset: function() {
+            this.length = 0;
+            this.models = [];
+            this._byId  = {};
+        },
+
+        // Prepare a hash of attributes (or other model) to be added to this
+        // collection.
+        _prepareModel: function(attrs, options) {
+            if (attrs instanceof Model) {
+                if (!attrs.collection) attrs.collection = this;
+                return attrs;
+            }
+            options = options ? _.clone(options) : {};
+            options.collection = this;
+            var model = new this.model(attrs, options);
+            if (!model.validationError) return model;
+            this.trigger('invalid', this, model.validationError, options);
+            return false;
+        },
+
+        // Internal method to sever a model's ties to a collection.
+        _removeReference: function(model) {
+            if (this === model.collection) delete model.collection;
+            model.off('all', this._onModelEvent, this);
+        },
+
+        // Internal method called every time a model in the set fires an event.
+        // Sets need to update their indexes when models change ids. All other
+        // events simply proxy through. "add" and "remove" events that originate
+        // in other collections are ignored.
+        _onModelEvent: function(event, model, collection, options) {
+            if ((event === 'add' || event === 'remove') && collection !== this) return;
+            if (event === 'destroy') this.remove(model, options);
+            if (model && event === 'change:' + model.idAttribute) {
+                delete this._byId[model.previous(model.idAttribute)];
+                if (model.id != null) this._byId[model.id] = model;
+            }
+            this.trigger.apply(this, arguments);
+        }
+
+    });
+
+    // Underscore methods that we want to implement on the Collection.
+    // 90% of the core usefulness of Backbone Collections is actually implemented
+    // right here:
+    var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
+        'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
+        'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
+        'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
+        'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
+        'lastIndexOf', 'isEmpty', 'chain'];
+
+    // Mix in each Underscore method as a proxy to `Collection#models`.
+    _.each(methods, function(method) {
+        Collection.prototype[method] = function() {
+            var args = slice.call(arguments);
+            args.unshift(this.models);
+            return _[method].apply(_, args);
+        };
+    });
+
+    // Underscore methods that take a property name as an argument.
+    var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
+
+    // Use attributes instead of properties.
+    _.each(attributeMethods, function(method) {
+        Collection.prototype[method] = function(value, context) {
+            var iterator = _.isFunction(value) ? value : function(model) {
+                return model.get(value);
+            };
+            return _[method](this.models, iterator, context);
+        };
+    });
+
+    // Backbone.View
+    // -------------
+
+    // Backbone Views are almost more convention than they are actual code. A View
+    // is simply a JavaScript object that represents a logical chunk of UI in the
+    // DOM. This might be a single item, an entire list, a sidebar or panel, or
+    // even the surrounding frame which wraps your whole app. Defining a chunk of
+    // UI as a **View** allows you to define your DOM events declaratively, without
+    // having to worry about render order ... and makes it easy for the view to
+    // react to specific changes in the state of your models.
+
+    // Creating a Backbone.View creates its initial element outside of the DOM,
+    // if an existing element is not provided...
+    var View = Backbone.View = function(options) {
+        this.cid = _.uniqueId('view');
+        options || (options = {});
+        _.extend(this, _.pick(options, viewOptions));
+        this._ensureElement();
+        this.initialize.apply(this, arguments);
+        this.delegateEvents();
+    };
+
+    // Cached regex to split keys for `delegate`.
+    var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+    // List of view options to be merged as properties.
+    var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
+
+    // Set up all inheritable **Backbone.View** properties and methods.
+    _.extend(View.prototype, Events, {
+
+        // The default `tagName` of a View's element is `"div"`.
+        tagName: 'div',
+
+        // jQuery delegate for element lookup, scoped to DOM elements within the
+        // current view. This should be preferred to global lookups where possible.
+        $: function(selector) {
+            return this.$el.find(selector);
+        },
+
+        // Initialize is an empty function by default. Override it with your own
+        // initialization logic.
+        initialize: function(){},
+
+        // **render** is the core function that your view should override, in order
+        // to populate its element (`this.el`), with the appropriate HTML. The
+        // convention is for **render** to always return `this`.
+        render: function() {
+            return this;
+        },
+
+        // Remove this view by taking the element out of the DOM, and removing any
+        // applicable Backbone.Events listeners.
+        remove: function() {
+            this.$el.remove();
+            this.stopListening();
+            return this;
+        },
+
+        // Change the view's element (`this.el` property), including event
+        // re-delegation.
+        setElement: function(element, delegate) {
+            if (this.$el) this.undelegateEvents();
+            this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
+            this.el = this.$el[0];
+            if (delegate !== false) this.delegateEvents();
+            return this;
+        },
+
+        // Set callbacks, where `this.events` is a hash of
+        //
+        // *{"event selector": "callback"}*
+        //
+        //     {
+        //       'mousedown .title':  'edit',
+        //       'click .button':     'save',
+        //       'click .open':       function(e) { ... }
+        //     }
+        //
+        // pairs. Callbacks will be bound to the view, with `this` set properly.
+        // Uses event delegation for efficiency.
+        // Omitting the selector binds the event to `this.el`.
+        // This only works for delegate-able events: not `focus`, `blur`, and
+        // not `change`, `submit`, and `reset` in Internet Explorer.
+        delegateEvents: function(events) {
+            if (!(events || (events = _.result(this, 'events')))) return this;
+            this.undelegateEvents();
+            for (var key in events) {
+                var method = events[key];
+                if (!_.isFunction(method)) method = this[events[key]];
+                if (!method) continue;
+
+                var match = key.match(delegateEventSplitter);
+                var eventName = match[1], selector = match[2];
+                method = _.bind(method, this);
+                eventName += '.delegateEvents' + this.cid;
+                if (selector === '') {
+                    this.$el.on(eventName, method);
+                } else {
+                    this.$el.on(eventName, selector, method);
+                }
+            }
+            return this;
+        },
+
+        // Clears all callbacks previously bound to the view with `delegateEvents`.
+        // You usually don't need to use this, but may wish to if you have multiple
+        // Backbone views attached to the same DOM element.
+        undelegateEvents: function() {
+            this.$el.off('.delegateEvents' + this.cid);
+            return this;
+        },
+
+        // Ensure that the View has a DOM element to render into.
+        // If `this.el` is a string, pass it through `$()`, take the first
+        // matching element, and re-assign it to `el`. Otherwise, create
+        // an element from the `id`, `className` and `tagName` properties.
+        _ensureElement: function() {
+            if (!this.el) {
+                var attrs = _.extend({}, _.result(this, 'attributes'));
+                if (this.id) attrs.id = _.result(this, 'id');
+                if (this.className) attrs['class'] = _.result(this, 'className');
+                var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
+                this.setElement($el, false);
+            } else {
+                this.setElement(_.result(this, 'el'), false);
+            }
+        }
+
+    });
+
+    // Backbone.sync
+    // -------------
+
+    // Override this function to change the manner in which Backbone persists
+    // models to the server. You will be passed the type of request, and the
+    // model in question. By default, makes a RESTful Ajax request
+    // to the model's `url()`. Some possible customizations could be:
+    //
+    // * Use `setTimeout` to batch rapid-fire updates into a single request.
+    // * Send up the models as XML instead of JSON.
+    // * Persist models via WebSockets instead of Ajax.
+    //
+    // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+    // as `POST`, with a `_method` parameter containing the true HTTP method,
+    // as well as all requests with the body as `application/x-www-form-urlencoded`
+    // instead of `application/json` with the model in a param named `model`.
+    // Useful when interfacing with server-side languages like **PHP** that make
+    // it difficult to read the body of `PUT` requests.
+    Backbone.sync = function(method, model, options) {
+        var type = methodMap[method];
+
+        // Default options, unless specified.
+        _.defaults(options || (options = {}), {
+            emulateHTTP: Backbone.emulateHTTP,
+            emulateJSON: Backbone.emulateJSON
+        });
+
+        // Default JSON-request options.
+        var params = {type: type, dataType: 'json'};
+
+        // Ensure that we have a URL.
+        if (!options.url) {
+            params.url = _.result(model, 'url') || urlError();
+        }
+
+        // Ensure that we have the appropriate request data.
+        if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+            params.contentType = 'application/json';
+            params.data = JSON.stringify(options.attrs || model.toJSON(options));
+        }
+
+        // For older servers, emulate JSON by encoding the request into an HTML-form.
+        if (options.emulateJSON) {
+            params.contentType = 'application/x-www-form-urlencoded';
+            params.data = params.data ? {model: params.data} : {};
+        }
+
+        // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+        // And an `X-HTTP-Method-Override` header.
+        if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
+            params.type = 'POST';
+            if (options.emulateJSON) params.data._method = type;
+            var beforeSend = options.beforeSend;
+            options.beforeSend = function(xhr) {
+                xhr.setRequestHeader('X-HTTP-Method-Override', type);
+                if (beforeSend) return beforeSend.apply(this, arguments);
+            };
+        }
+
+        // Don't process data on a non-GET request.
+        if (params.type !== 'GET' && !options.emulateJSON) {
+            params.processData = false;
+        }
+
+        // If we're sending a `PATCH` request, and we're in an old Internet Explorer
+        // that still has ActiveX enabled by default, override jQuery to use that
+        // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
+        if (params.type === 'PATCH' && noXhrPatch) {
+            params.xhr = function() {
+                return new ActiveXObject("Microsoft.XMLHTTP");
+            };
+        }
+
+        // Make the request, allowing the user to override any Ajax options.
+        var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
+        model.trigger('request', model, xhr, options);
+        return xhr;
+    };
+
+    var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
+
+    // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+    var methodMap = {
+        'create': 'POST',
+        'update': 'PUT',
+        'patch':  'PATCH',
+        'delete': 'DELETE',
+        'read':   'GET'
+    };
+
+    // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
+    // Override this if you'd like to use a different library.
+    Backbone.ajax = function() {
+        return Backbone.$.ajax.apply(Backbone.$, arguments);
+    };
+
+    // Backbone.Router
+    // ---------------
+
+    // Routers map faux-URLs to actions, and fire events when routes are
+    // matched. Creating a new one sets its `routes` hash, if not set statically.
+    var Router = Backbone.Router = function(options) {
+        options || (options = {});
+        if (options.routes) this.routes = options.routes;
+        this._bindRoutes();
+        this.initialize.apply(this, arguments);
+    };
+
+    // Cached regular expressions for matching named param parts and splatted
+    // parts of route strings.
+    var optionalParam = /\((.*?)\)/g;
+    var namedParam    = /(\(\?)?:\w+/g;
+    var splatParam    = /\*\w+/g;
+    var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
+
+    // Set up all inheritable **Backbone.Router** properties and methods.
+    _.extend(Router.prototype, Events, {
+
+        // Initialize is an empty function by default. Override it with your own
+        // initialization logic.
+        initialize: function(){},
+
+        // Manually bind a single named route to a callback. For example:
+        //
+        //     this.route('search/:query/p:num', 'search', function(query, num) {
+        //       ...
+        //     });
+        //
+        route: function(route, name, callback) {
+            if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+            if (_.isFunction(name)) {
+                callback = name;
+                name = '';
+            }
+            if (!callback) callback = this[name];
+            var router = this;
+            Backbone.history.route(route, function(fragment) {
+                var args = router._extractParameters(route, fragment);
+                callback && callback.apply(router, args);
+                router.trigger.apply(router, ['route:' + name].concat(args));
+                router.trigger('route', name, args);
+                Backbone.history.trigger('route', router, name, args);
+            });
+            return this;
+        },
+
+        // Simple proxy to `Backbone.history` to save a fragment into the history.
+        navigate: function(fragment, options) {
+            Backbone.history.navigate(fragment, options);
+            return this;
+        },
+
+        // Bind all defined routes to `Backbone.history`. We have to reverse the
+        // order of the routes here to support behavior where the most general
+        // routes can be defined at the bottom of the route map.
+        _bindRoutes: function() {
+            if (!this.routes) return;
+            this.routes = _.result(this, 'routes');
+            var route, routes = _.keys(this.routes);
+            while ((route = routes.pop()) != null) {
+                this.route(route, this.routes[route]);
+            }
+        },
+
+        // Convert a route string into a regular expression, suitable for matching
+        // against the current location hash.
+        _routeToRegExp: function(route) {
+            route = route.replace(escapeRegExp, '\\$&')
+                .replace(optionalParam, '(?:$1)?')
+                .replace(namedParam, function(match, optional) {
+                    return optional ? match : '([^\/]+)';
+                })
+                .replace(splatParam, '(.*?)');
+            return new RegExp('^' + route + '$');
+        },
+
+        // Given a route, and a URL fragment that it matches, return the array of
+        // extracted decoded parameters. Empty or unmatched parameters will be
+        // treated as `null` to normalize cross-browser behavior.
+        _extractParameters: function(route, fragment) {
+            var params = route.exec(fragment).slice(1);
+            return _.map(params, function(param) {
+                return param ? decodeURIComponent(param) : null;
+            });
+        }
+
+    });
+
+    // Backbone.History
+    // ----------------
+
+    // Handles cross-browser history management, based on either
+    // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
+    // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
+    // and URL fragments. If the browser supports neither (old IE, natch),
+    // falls back to polling.
+    var History = Backbone.History = function() {
+        this.handlers = [];
+        _.bindAll(this, 'checkUrl');
+
+        // Ensure that `History` can be used outside of the browser.
+        if (typeof window !== 'undefined') {
+            this.location = window.location;
+            this.history = window.history;
+        }
+    };
+
+    // Cached regex for stripping a leading hash/slash and trailing space.
+    var routeStripper = /^[#\/]|\s+$/g;
+
+    // Cached regex for stripping leading and trailing slashes.
+    var rootStripper = /^\/+|\/+$/g;
+
+    // Cached regex for detecting MSIE.
+    var isExplorer = /msie [\w.]+/;
+
+    // Cached regex for removing a trailing slash.
+    var trailingSlash = /\/$/;
+
+    // Cached regex for stripping urls of hash and query.
+    var pathStripper = /[?#].*$/;
+
+    // Has the history handling already been started?
+    History.started = false;
+
+    // Set up all inheritable **Backbone.History** properties and methods.
+    _.extend(History.prototype, Events, {
+
+        // The default interval to poll for hash changes, if necessary, is
+        // twenty times a second.
+        interval: 50,
+
+        // Gets the true hash value. Cannot use location.hash directly due to bug
+        // in Firefox where location.hash will always be decoded.
+        getHash: function(window) {
+            var match = (window || this).location.href.match(/#(.*)$/);
+            return match ? match[1] : '';
+        },
+
+        // Get the cross-browser normalized URL fragment, either from the URL,
+        // the hash, or the override.
+        getFragment: function(fragment, forcePushState) {
+            if (fragment == null) {
+                if (this._hasPushState || !this._wantsHashChange || forcePushState) {
+                    fragment = this.location.pathname;
+                    var root = this.root.replace(trailingSlash, '');
+                    if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
+                } else {
+                    fragment = this.getHash();
+                }
+            }
+            return fragment.replace(routeStripper, '');
+        },
+
+        // Start the hash change handling, returning `true` if the current URL matches
+        // an existing route, and `false` otherwise.
+        start: function(options) {
+            if (History.started) throw new Error("Backbone.history has already been started");
+            History.started = true;
+
+            // Figure out the initial configuration. Do we need an iframe?
+            // Is pushState desired ... is it available?
+            this.options          = _.extend({root: '/'}, this.options, options);
+            this.root             = this.options.root;
+            this._wantsHashChange = this.options.hashChange !== false;
+            this._wantsPushState  = !!this.options.pushState;
+            this._hasPushState    = !!(this.options.pushState && this.history && this.history.pushState);
+            var fragment          = this.getFragment();
+            var docMode           = document.documentMode;
+            var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+
+            // Normalize root to always include a leading and trailing slash.
+            this.root = ('/' + this.root + '/').replace(rootStripper, '/');
+
+            if (oldIE && this._wantsHashChange) {
+                this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
+                this.navigate(fragment);
+            }
+
+            // Depending on whether we're using pushState or hashes, and whether
+            // 'onhashchange' is supported, determine how we check the URL state.
+            if (this._hasPushState) {
+                Backbone.$(window).on('popstate', this.checkUrl);
+            } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+                Backbone.$(window).on('hashchange', this.checkUrl);
+            } else if (this._wantsHashChange) {
+                this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+            }
+
+            // Determine if we need to change the base url, for a pushState link
+            // opened by a non-pushState browser.
+            this.fragment = fragment;
+            var loc = this.location;
+            var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
+
+            // Transition from hashChange to pushState or vice versa if both are
+            // requested.
+            if (this._wantsHashChange && this._wantsPushState) {
+
+                // If we've started off with a route from a `pushState`-enabled
+                // browser, but we're currently in a browser that doesn't support it...
+                if (!this._hasPushState && !atRoot) {
+                    this.fragment = this.getFragment(null, true);
+                    this.location.replace(this.root + this.location.search + '#' + this.fragment);
+                    // Return immediately as browser will do redirect to new url
+                    return true;
+
+                    // Or if we've started out with a hash-based route, but we're currently
+                    // in a browser where it could be `pushState`-based instead...
+                } else if (this._hasPushState && atRoot && loc.hash) {
+                    this.fragment = this.getHash().replace(routeStripper, '');
+                    this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
+                }
+
+            }
+
+            if (!this.options.silent) return this.loadUrl();
+        },
+
+        // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+        // but possibly useful for unit testing Routers.
+        stop: function() {
+            Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
+            clearInterval(this._checkUrlInterval);
+            History.started = false;
+        },
+
+        // Add a route to be tested when the fragment changes. Routes added later
+        // may override previous routes.
+        route: function(route, callback) {
+            this.handlers.unshift({route: route, callback: callback});
+        },
+
+        // Checks the current URL to see if it has changed, and if it has,
+        // calls `loadUrl`, normalizing across the hidden iframe.
+        checkUrl: function(e) {
+            var current = this.getFragment();
+            if (current === this.fragment && this.iframe) {
+                current = this.getFragment(this.getHash(this.iframe));
+            }
+            if (current === this.fragment) return false;
+            if (this.iframe) this.navigate(current);
+            this.loadUrl();
+        },
+
+        // Attempt to load the current URL fragment. If a route succeeds with a
+        // match, returns `true`. If no defined routes matches the fragment,
+        // returns `false`.
+        loadUrl: function(fragment) {
+            fragment = this.fragment = this.getFragment(fragment);
+            return _.any(this.handlers, function(handler) {
+                if (handler.route.test(fragment)) {
+                    handler.callback(fragment);
+                    return true;
+                }
+            });
+        },
+
+        // Save a fragment into the hash history, or replace the URL state if the
+        // 'replace' option is passed. You are responsible for properly URL-encoding
+        // the fragment in advance.
+        //
+        // The options object can contain `trigger: true` if you wish to have the
+        // route callback be fired (not usually desirable), or `replace: true`, if
+        // you wish to modify the current URL without adding an entry to the history.
+        navigate: function(fragment, options) {
+            if (!History.started) return false;
+            if (!options || options === true) options = {trigger: !!options};
+
+            var url = this.root + (fragment = this.getFragment(fragment || ''));
+
+            // Strip the fragment of the query and hash for matching.
+            fragment = fragment.replace(pathStripper, '');
+
+            if (this.fragment === fragment) return;
+            this.fragment = fragment;
+
+            // Don't include a trailing slash on the root.
+            if (fragment === '' && url !== '/') url = url.slice(0, -1);
+
+            // If pushState is available, we use it to set the fragment as a real URL.
+            if (this._hasPushState) {
+                this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+                // If hash changes haven't been explicitly disabled, update the hash
+                // fragment to store history.
+            } else if (this._wantsHashChange) {
+                this._updateHash(this.location, fragment, options.replace);
+                if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
+                    // Opening and closing the iframe tricks IE7 and earlier to push a
+                    // history entry on hash-tag change.  When replace is true, we don't
+                    // want this.
+                    if(!options.replace) this.iframe.document.open().close();
+                    this._updateHash(this.iframe.location, fragment, options.replace);
+                }
+
+                // If you've told us that you explicitly don't want fallback hashchange-
+                // based history, then `navigate` becomes a page refresh.
+            } else {
+                return this.location.assign(url);
+            }
+            if (options.trigger) return this.loadUrl(fragment);
+        },
+
+        // Update the hash location, either replacing the current entry, or adding
+        // a new one to the browser history.
+        _updateHash: function(location, fragment, replace) {
+            if (replace) {
+                var href = location.href.replace(/(javascript:|#).*$/, '');
+                location.replace(href + '#' + fragment);
+            } else {
+                // Some browsers require that `hash` contains a leading #.
+                location.hash = '#' + fragment;
+            }
+        }
+
+    });
+
+    // Create the default Backbone.history.
+    Backbone.history = new History;
+
+    // Helpers
+    // -------
+
+    // Helper function to correctly set up the prototype chain, for subclasses.
+    // Similar to `goog.inherits`, but uses a hash of prototype properties and
+    // class properties to be extended.
+    var extend = function(protoProps, staticProps) {
+        var parent = this;
+        var child;
+
+        // The constructor function for the new subclass is either defined by you
+        // (the "constructor" property in your `extend` definition), or defaulted
+        // by us to simply call the parent's constructor.
+        if (protoProps && _.has(protoProps, 'constructor')) {
+            child = protoProps.constructor;
+        } else {
+            child = function(){ return parent.apply(this, arguments); };
+        }
+
+        // Add static properties to the constructor function, if supplied.
+        _.extend(child, parent, staticProps);
+
+        // Set the prototype chain to inherit from `parent`, without calling
+        // `parent`'s constructor function.
+        var Surrogate = function(){ this.constructor = child; };
+        Surrogate.prototype = parent.prototype;
+        child.prototype = new Surrogate;
+
+        // Add prototype properties (instance properties) to the subclass,
+        // if supplied.
+        if (protoProps) _.extend(child.prototype, protoProps);
+
+        // Set a convenience property in case the parent's prototype is needed
+        // later.
+        child.__super__ = parent.prototype;
+
+        return child;
+    };
+
+    // Set up inheritance for the model, collection, router, view and history.
+    Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
+
+    // Throw an error when a URL is needed, and none is supplied.
+    var urlError = function() {
+        throw new Error('A "url" property or function must be specified');
+    };
+
+    // Wrap an optional error callback with a fallback error event.
+    var wrapError = function(model, options) {
+        var error = options.error;
+        options.error = function(resp) {
+            if (error) error(model, resp, options);
+            model.trigger('error', model, resp, options);
+        };
+    };
+
+}).call(this);

+ 631 - 0
ambari-web/vendor/scripts/jquery.ui.autocomplete.js

@@ -0,0 +1,631 @@
+/*!
+ * jQuery UI Autocomplete 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ */
+(function( $, undefined ) {
+
+// used to prevent race conditions with remote data sources
+var requestIndex = 0;
+
+$.widget( "ui.autocomplete", {
+	options: {
+		appendTo: "body",
+		autoFocus: false,
+		delay: 300,
+		minLength: 1,
+		position: {
+			my: "left top",
+			at: "left bottom",
+			collision: "none"
+		},
+		source: null
+	},
+
+	pending: 0,
+
+	_create: function() {
+		var self = this,
+			doc = this.element[ 0 ].ownerDocument,
+			suppressKeyPress;
+		this.isMultiLine = this.element.is( "textarea" );
+
+		this.element
+			.addClass( "ui-autocomplete-input" )
+			.attr( "autocomplete", "off" )
+			// TODO verify these actually work as intended
+			.attr({
+				role: "textbox",
+				"aria-autocomplete": "list",
+				"aria-haspopup": "true"
+			})
+			.bind( "keydown.autocomplete", function( event ) {
+				if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
+					return;
+				}
+
+				suppressKeyPress = false;
+				var keyCode = $.ui.keyCode;
+				switch( event.keyCode ) {
+				case keyCode.PAGE_UP:
+					self._move( "previousPage", event );
+					break;
+				case keyCode.PAGE_DOWN:
+					self._move( "nextPage", event );
+					break;
+				case keyCode.UP:
+					self._keyEvent( "previous", event );
+					break;
+				case keyCode.DOWN:
+					self._keyEvent( "next", event );
+					break;
+				case keyCode.ENTER:
+				case keyCode.NUMPAD_ENTER:
+					// when menu is open and has focus
+					if ( self.menu.active ) {
+						// #6055 - Opera still allows the keypress to occur
+						// which causes forms to submit
+						suppressKeyPress = true;
+						event.preventDefault();
+					}
+					//passthrough - ENTER and TAB both select the current element
+				case keyCode.TAB:
+					if ( !self.menu.active ) {
+						return;
+					}
+					self.menu.select( event );
+					break;
+				case keyCode.ESCAPE:
+					self.element.val( self.term );
+					self.close( event );
+					break;
+				default:
+					// keypress is triggered before the input value is changed
+					clearTimeout( self.searching );
+					self.searching = setTimeout(function() {
+						// only search if the value has changed
+						if ( self.term != self.element.val() ) {
+							self.selectedItem = null;
+							self.search( null, event );
+						}
+					}, self.options.delay );
+					break;
+				}
+			})
+			.bind( "keypress.autocomplete", function( event ) {
+				if ( suppressKeyPress ) {
+					suppressKeyPress = false;
+					event.preventDefault();
+				}
+			})
+			.bind( "focus.autocomplete", function() {
+				if ( self.options.disabled ) {
+					return;
+				}
+
+				self.selectedItem = null;
+				self.previous = self.element.val();
+			})
+			.bind( "blur.autocomplete", function( event ) {
+				if ( self.options.disabled ) {
+					return;
+				}
+
+				clearTimeout( self.searching );
+				// clicks on the menu (or a button to trigger a search) will cause a blur event
+				self.closing = setTimeout(function() {
+					self.close( event );
+					self._change( event );
+				}, 150 );
+			});
+		this._initSource();
+		this.menu = $( "<ul></ul>" )
+			.addClass( "ui-autocomplete" )
+			.appendTo( $( this.options.appendTo || "body", doc )[0] )
+			// prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
+			.mousedown(function( event ) {
+				// clicking on the scrollbar causes focus to shift to the body
+				// but we can't detect a mouseup or a click immediately afterward
+				// so we have to track the next mousedown and close the menu if
+				// the user clicks somewhere outside of the autocomplete
+				var menuElement = self.menu.element[ 0 ];
+				if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
+					setTimeout(function() {
+						$( document ).one( 'mousedown', function( event ) {
+							if ( event.target !== self.element[ 0 ] &&
+								event.target !== menuElement &&
+								!$.ui.contains( menuElement, event.target ) ) {
+								self.close();
+							}
+						});
+					}, 1 );
+				}
+
+				// use another timeout to make sure the blur-event-handler on the input was already triggered
+				setTimeout(function() {
+					clearTimeout( self.closing );
+				}, 13);
+			})
+			.menu({
+				focus: function( event, ui ) {
+					var item = ui.item.data( "item.autocomplete" );
+					if ( false !== self._trigger( "focus", event, { item: item } ) ) {
+						// use value to match what will end up in the input, if it was a key event
+						if ( /^key/.test(event.originalEvent.type) ) {
+							self.element.val( item.value );
+						}
+					}
+				},
+				selected: function( event, ui ) {
+					var item = ui.item.data( "item.autocomplete" ),
+						previous = self.previous;
+
+					// only trigger when focus was lost (click on menu)
+					if ( self.element[0] !== doc.activeElement ) {
+						self.element.focus();
+						self.previous = previous;
+						// #6109 - IE triggers two focus events and the second
+						// is asynchronous, so we need to reset the previous
+						// term synchronously and asynchronously :-(
+						setTimeout(function() {
+							self.previous = previous;
+							self.selectedItem = item;
+						}, 1);
+					}
+
+					if ( false !== self._trigger( "select", event, { item: item } ) ) {
+						self.element.val( item.value );
+					}
+					// reset the term after the select event
+					// this allows custom select handling to work properly
+					self.term = self.element.val();
+
+					self.close( event );
+					self.selectedItem = item;
+				},
+				blur: function( event, ui ) {
+					// don't set the value of the text field if it's already correct
+					// this prevents moving the cursor unnecessarily
+					if ( self.menu.element.is(":visible") &&
+						( self.element.val() !== self.term ) ) {
+						self.element.val( self.term );
+					}
+				}
+			})
+			.zIndex( this.element.zIndex() + 1 )
+			// workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
+			.css({ top: 0, left: 0 })
+			.hide()
+			.data( "menu" );
+		if ( $.fn.bgiframe ) {
+			 this.menu.element.bgiframe();
+		}
+		// turning off autocomplete prevents the browser from remembering the
+		// value when navigating through history, so we re-enable autocomplete
+		// if the page is unloaded before the widget is destroyed. #7790
+		self.beforeunloadHandler = function() {
+			self.element.removeAttr( "autocomplete" );
+		};
+		$( window ).bind( "beforeunload", self.beforeunloadHandler );
+	},
+
+	destroy: function() {
+		this.element
+			.removeClass( "ui-autocomplete-input" )
+			.removeAttr( "autocomplete" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-autocomplete" )
+			.removeAttr( "aria-haspopup" );
+		this.menu.element.remove();
+		$( window ).unbind( "beforeunload", this.beforeunloadHandler );
+		$.Widget.prototype.destroy.call( this );
+	},
+
+	_setOption: function( key, value ) {
+		$.Widget.prototype._setOption.apply( this, arguments );
+		if ( key === "source" ) {
+			this._initSource();
+		}
+		if ( key === "appendTo" ) {
+			this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
+		}
+		if ( key === "disabled" && value && this.xhr ) {
+			this.xhr.abort();
+		}
+	},
+
+	_initSource: function() {
+		var self = this,
+			array,
+			url;
+		if ( $.isArray(this.options.source) ) {
+			array = this.options.source;
+			this.source = function( request, response ) {
+				response( $.ui.autocomplete.filter(array, request.term) );
+			};
+		} else if ( typeof this.options.source === "string" ) {
+			url = this.options.source;
+			this.source = function( request, response ) {
+				if ( self.xhr ) {
+					self.xhr.abort();
+				}
+				self.xhr = $.ajax({
+					url: url,
+					data: request,
+					dataType: "json",
+					success: function( data, status ) {
+						response( data );
+					},
+					error: function() {
+						response( [] );
+					}
+				});
+			};
+		} else {
+			this.source = this.options.source;
+		}
+	},
+
+	search: function( value, event ) {
+		value = value != null ? value : this.element.val();
+
+		// always save the actual value, not the one passed as an argument
+		this.term = this.element.val();
+
+		if ( value.length < this.options.minLength ) {
+			return this.close( event );
+		}
+
+		clearTimeout( this.closing );
+		if ( this._trigger( "search", event ) === false ) {
+			return;
+		}
+
+		return this._search( value );
+	},
+
+	_search: function( value ) {
+		this.pending++;
+		this.element.addClass( "ui-autocomplete-loading" );
+
+		this.source( { term: value }, this._response() );
+	},
+
+	_response: function() {
+		var that = this,
+			index = ++requestIndex;
+
+		return function( content ) {
+			if ( index === requestIndex ) {
+				that.__response( content );
+			}
+
+			that.pending--;
+			if ( !that.pending ) {
+				that.element.removeClass( "ui-autocomplete-loading" );
+			}
+		};
+	},
+
+	__response: function( content ) {
+		if ( !this.options.disabled && content && content.length ) {
+			content = this._normalize( content );
+			this._suggest( content );
+			this._trigger( "open" );
+		} else {
+			this.close();
+		}
+	},
+
+	close: function( event ) {
+		clearTimeout( this.closing );
+		if ( this.menu.element.is(":visible") ) {
+			this.menu.element.hide();
+			this.menu.deactivate();
+			this._trigger( "close", event );
+		}
+	},
+	
+	_change: function( event ) {
+		if ( this.previous !== this.element.val() ) {
+			this._trigger( "change", event, { item: this.selectedItem } );
+		}
+	},
+
+	_normalize: function( items ) {
+		// assume all items have the right format when the first item is complete
+		if ( items.length && items[0].label && items[0].value ) {
+			return items;
+		}
+		return $.map( items, function(item) {
+			if ( typeof item === "string" ) {
+				return {
+					label: item,
+					value: item
+				};
+			}
+			return $.extend({
+				label: item.label || item.value,
+				value: item.value || item.label
+			}, item );
+		});
+	},
+
+	_suggest: function( items ) {
+		var ul = this.menu.element
+			.empty()
+			.zIndex( this.element.zIndex() + 1 );
+		this._renderMenu( ul, items );
+		// TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
+		this.menu.deactivate();
+		this.menu.refresh();
+
+		// size and position menu
+		ul.show();
+		this._resizeMenu();
+		ul.position( $.extend({
+			of: this.element
+		}, this.options.position ));
+
+		if ( this.options.autoFocus ) {
+			this.menu.next( new $.Event("mouseover") );
+		}
+	},
+
+	_resizeMenu: function() {
+		var ul = this.menu.element;
+		ul.outerWidth( Math.max(
+			// Firefox wraps long text (possibly a rounding bug)
+			// so we add 1px to avoid the wrapping (#7513)
+			ul.width( "" ).outerWidth() + 1,
+			this.element.outerWidth()
+		) );
+	},
+
+	_renderMenu: function( ul, items ) {
+		var self = this;
+		$.each( items, function( index, item ) {
+			self._renderItem( ul, item );
+		});
+	},
+
+	_renderItem: function( ul, item) {
+		return $( "<li></li>" )
+			.data( "item.autocomplete", item )
+			.append( $( "<a></a>" ).text( item.label ) )
+			.appendTo( ul );
+	},
+
+	_move: function( direction, event ) {
+		if ( !this.menu.element.is(":visible") ) {
+			this.search( null, event );
+			return;
+		}
+		if ( this.menu.first() && /^previous/.test(direction) ||
+				this.menu.last() && /^next/.test(direction) ) {
+			this.element.val( this.term );
+			this.menu.deactivate();
+			return;
+		}
+		this.menu[ direction ]( event );
+	},
+
+	widget: function() {
+		return this.menu.element;
+	},
+	_keyEvent: function( keyEvent, event ) {
+		if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
+			this._move( keyEvent, event );
+
+			// prevents moving cursor to beginning/end of the text field in some browsers
+			event.preventDefault();
+		}
+	}
+});
+
+$.extend( $.ui.autocomplete, {
+	escapeRegex: function( value ) {
+		return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+	},
+	filter: function(array, term) {
+		var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
+		return $.grep( array, function(value) {
+			return matcher.test( value.label || value.value || value );
+		});
+	}
+});
+
+}( jQuery ));
+
+/*
+ * jQuery UI Menu (not officially released)
+ * 
+ * This widget isn't yet finished and the API is subject to change. We plan to finish
+ * it for the next release. You're welcome to give it a try anyway and give us feedback,
+ * as long as you're okay with migrating your code later on. We can help with that, too.
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *  jquery.ui.widget.js
+ */
+(function($) {
+
+$.widget("ui.menu", {
+	_create: function() {
+		var self = this;
+		this.element
+			.addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
+			.attr({
+				role: "listbox",
+				"aria-activedescendant": "ui-active-menuitem"
+			})
+			.click(function( event ) {
+				if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
+					return;
+				}
+				// temporary
+				event.preventDefault();
+				self.select( event );
+			});
+		this.refresh();
+	},
+	
+	refresh: function() {
+		var self = this;
+
+		// don't refresh list items that are already adapted
+		var items = this.element.children("li:not(.ui-menu-item):has(a)")
+			.addClass("ui-menu-item")
+			.attr("role", "menuitem");
+		
+		items.children("a")
+			.addClass("ui-corner-all")
+			.attr("tabindex", -1)
+			// mouseenter doesn't work with event delegation
+			.mouseenter(function( event ) {
+				self.activate( event, $(this).parent() );
+			})
+			.mouseleave(function() {
+				self.deactivate();
+			});
+	},
+
+	activate: function( event, item ) {
+		this.deactivate();
+		if (this.hasScroll()) {
+			var offset = item.offset().top - this.element.offset().top,
+				scroll = this.element.scrollTop(),
+				elementHeight = this.element.height();
+			if (offset < 0) {
+				this.element.scrollTop( scroll + offset);
+			} else if (offset >= elementHeight) {
+				this.element.scrollTop( scroll + offset - elementHeight + item.height());
+			}
+		}
+		this.active = item.eq(0)
+			.children("a")
+				.addClass("ui-state-hover")
+				.attr("id", "ui-active-menuitem")
+			.end();
+		this._trigger("focus", event, { item: item });
+	},
+
+	deactivate: function() {
+		if (!this.active) { return; }
+
+		this.active.children("a")
+			.removeClass("ui-state-hover")
+			.removeAttr("id");
+		this._trigger("blur");
+		this.active = null;
+	},
+
+	next: function(event) {
+		this.move("next", ".ui-menu-item:first", event);
+	},
+
+	previous: function(event) {
+		this.move("prev", ".ui-menu-item:last", event);
+	},
+
+	first: function() {
+		return this.active && !this.active.prevAll(".ui-menu-item").length;
+	},
+
+	last: function() {
+		return this.active && !this.active.nextAll(".ui-menu-item").length;
+	},
+
+	move: function(direction, edge, event) {
+		if (!this.active) {
+			this.activate(event, this.element.children(edge));
+			return;
+		}
+		var next = this.active[direction + "All"](".ui-menu-item").eq(0);
+		if (next.length) {
+			this.activate(event, next);
+		} else {
+			this.activate(event, this.element.children(edge));
+		}
+	},
+
+	// TODO merge with previousPage
+	nextPage: function(event) {
+		if (this.hasScroll()) {
+			// TODO merge with no-scroll-else
+			if (!this.active || this.last()) {
+				this.activate(event, this.element.children(".ui-menu-item:first"));
+				return;
+			}
+			var base = this.active.offset().top,
+				height = this.element.height(),
+				result = this.element.children(".ui-menu-item").filter(function() {
+					var close = $(this).offset().top - base - height + $(this).height();
+					// TODO improve approximation
+					return close < 10 && close > -10;
+				});
+
+			// TODO try to catch this earlier when scrollTop indicates the last page anyway
+			if (!result.length) {
+				result = this.element.children(".ui-menu-item:last");
+			}
+			this.activate(event, result);
+		} else {
+			this.activate(event, this.element.children(".ui-menu-item")
+				.filter(!this.active || this.last() ? ":first" : ":last"));
+		}
+	},
+
+	// TODO merge with nextPage
+	previousPage: function(event) {
+		if (this.hasScroll()) {
+			// TODO merge with no-scroll-else
+			if (!this.active || this.first()) {
+				this.activate(event, this.element.children(".ui-menu-item:last"));
+				return;
+			}
+
+			var base = this.active.offset().top,
+				height = this.element.height(),
+				result = this.element.children(".ui-menu-item").filter(function() {
+					var close = $(this).offset().top - base + height - $(this).height();
+					// TODO improve approximation
+					return close < 10 && close > -10;
+				});
+
+			// TODO try to catch this earlier when scrollTop indicates the last page anyway
+			if (!result.length) {
+				result = this.element.children(".ui-menu-item:first");
+			}
+			this.activate(event, result);
+		} else {
+			this.activate(event, this.element.children(".ui-menu-item")
+				.filter(!this.active || this.first() ? ":last" : ":first"));
+		}
+	},
+
+	hasScroll: function() {
+		return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
+	},
+
+	select: function( event ) {
+		this._trigger("selected", event, { item: this.active });
+	}
+});
+
+}(jQuery));

+ 308 - 0
ambari-web/vendor/scripts/jquery.ui.position.js

@@ -0,0 +1,308 @@
+/*!
+ * jQuery UI Position 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Position
+ */
+(function( $, undefined ) {
+
+$.ui = $.ui || {};
+
+var horizontalPositions = /left|center|right/,
+	verticalPositions = /top|center|bottom/,
+	center = "center",
+	support = {},
+	_position = $.fn.position,
+	_offset = $.fn.offset;
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var target = $( options.of ),
+		targetElem = target[0],
+		collision = ( options.collision || "flip" ).split( " " ),
+		offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ],
+		targetWidth,
+		targetHeight,
+		basePosition;
+
+	if ( targetElem.nodeType === 9 ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		basePosition = { top: 0, left: 0 };
+	// TODO: use $.isWindow() in 1.9
+	} else if ( targetElem.setTimeout ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		basePosition = { top: target.scrollTop(), left: target.scrollLeft() };
+	} else if ( targetElem.preventDefault ) {
+		// force left top to allow flipping
+		options.at = "left top";
+		targetWidth = targetHeight = 0;
+		basePosition = { top: options.of.pageY, left: options.of.pageX };
+	} else {
+		targetWidth = target.outerWidth();
+		targetHeight = target.outerHeight();
+		basePosition = target.offset();
+	}
+
+	// force my and at to have valid horizontal and veritcal positions
+	// if a value is missing or invalid, it will be converted to center 
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[this] || "" ).split( " " );
+		if ( pos.length === 1) {
+			pos = horizontalPositions.test( pos[0] ) ?
+				pos.concat( [center] ) :
+				verticalPositions.test( pos[0] ) ?
+					[ center ].concat( pos ) :
+					[ center, center ];
+		}
+		pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center;
+		pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center;
+		options[ this ] = pos;
+	});
+
+	// normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	// normalize offset option
+	offset[ 0 ] = parseInt( offset[0], 10 ) || 0;
+	if ( offset.length === 1 ) {
+		offset[ 1 ] = offset[ 0 ];
+	}
+	offset[ 1 ] = parseInt( offset[1], 10 ) || 0;
+
+	if ( options.at[0] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[0] === center ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[1] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[1] === center ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	basePosition.left += offset[ 0 ];
+	basePosition.top += offset[ 1 ];
+
+	return this.each(function() {
+		var elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0,
+			marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0,
+			collisionWidth = elemWidth + marginLeft +
+				( parseInt( $.curCSS( this, "marginRight", true ) ) || 0 ),
+			collisionHeight = elemHeight + marginTop +
+				( parseInt( $.curCSS( this, "marginBottom", true ) ) || 0 ),
+			position = $.extend( {}, basePosition ),
+			collisionPosition;
+
+		if ( options.my[0] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[0] === center ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[1] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[1] === center ) {
+			position.top -= elemHeight / 2;
+		}
+
+		// prevent fractions if jQuery version doesn't support them (see #5280)
+		if ( !support.fractions ) {
+			position.left = Math.round( position.left );
+			position.top = Math.round( position.top );
+		}
+
+		collisionPosition = {
+			left: position.left - marginLeft,
+			top: position.top - marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[i] ] ) {
+				$.ui.position[ collision[i] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: offset,
+					my: options.my,
+					at: options.at
+				});
+			}
+		});
+
+		if ( $.fn.bgiframe ) {
+			elem.bgiframe();
+		}
+		elem.offset( $.extend( position, { using: options.using } ) );
+	});
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var win = $( window ),
+				over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft();
+			position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left );
+		},
+		top: function( position, data ) {
+			var win = $( window ),
+				over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop();
+			position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top );
+		}
+	},
+
+	flip: {
+		left: function( position, data ) {
+			if ( data.at[0] === center ) {
+				return;
+			}
+			var win = $( window ),
+				over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(),
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					-data.targetWidth,
+				offset = -2 * data.offset[ 0 ];
+			position.left += data.collisionPosition.left < 0 ?
+				myOffset + atOffset + offset :
+				over > 0 ?
+					myOffset + atOffset + offset :
+					0;
+		},
+		top: function( position, data ) {
+			if ( data.at[1] === center ) {
+				return;
+			}
+			var win = $( window ),
+				over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(),
+				myOffset = data.my[ 1 ] === "top" ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					-data.targetHeight,
+				offset = -2 * data.offset[ 1 ];
+			position.top += data.collisionPosition.top < 0 ?
+				myOffset + atOffset + offset :
+				over > 0 ?
+					myOffset + atOffset + offset :
+					0;
+		}
+	}
+};
+
+// offset setter from jQuery 1.4
+if ( !$.offset.setOffset ) {
+	$.offset.setOffset = function( elem, options ) {
+		// set position first, in-case top/left are set even on static elem
+		if ( /static/.test( $.curCSS( elem, "position" ) ) ) {
+			elem.style.position = "relative";
+		}
+		var curElem   = $( elem ),
+			curOffset = curElem.offset(),
+			curTop    = parseInt( $.curCSS( elem, "top",  true ), 10 ) || 0,
+			curLeft   = parseInt( $.curCSS( elem, "left", true ), 10)  || 0,
+			props     = {
+				top:  (options.top  - curOffset.top)  + curTop,
+				left: (options.left - curOffset.left) + curLeft
+			};
+		
+		if ( 'using' in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	};
+
+	$.fn.offset = function( options ) {
+		var elem = this[ 0 ];
+		if ( !elem || !elem.ownerDocument ) { return null; }
+		if ( options ) {
+			if ( $.isFunction( options ) ) {
+				return this.each(function( i ) {
+					$( this ).offset( options.call( this, i, $( this ).offset() ) );
+				});
+			}
+			return this.each(function() {
+				$.offset.setOffset( this, options );
+			});
+		}
+		return _offset.call( this );
+	};
+}
+
+// jQuery <1.4.3 uses curCSS, in 1.4.3 - 1.7.2 curCSS = css, 1.8+ only has css
+if ( !$.curCSS ) {
+	$.curCSS = $.css;
+}
+
+// fraction support test (older versions of jQuery don't support fractions)
+(function () {
+	var body = document.getElementsByTagName( "body" )[ 0 ], 
+		div = document.createElement( "div" ),
+		testElement, testElementParent, testElementStyle, offset, offsetTotal;
+
+	//Create a "fake body" for testing based on method used in jQuery.support
+	testElement = document.createElement( body ? "div" : "body" );
+	testElementStyle = {
+		visibility: "hidden",
+		width: 0,
+		height: 0,
+		border: 0,
+		margin: 0,
+		background: "none"
+	};
+	if ( body ) {
+		$.extend( testElementStyle, {
+			position: "absolute",
+			left: "-1000px",
+			top: "-1000px"
+		});
+	}
+	for ( var i in testElementStyle ) {
+		testElement.style[ i ] = testElementStyle[ i ];
+	}
+	testElement.appendChild( div );
+	testElementParent = body || document.documentElement;
+	testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+	div.style.cssText = "position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;";
+
+	offset = $( div ).offset( function( _, offset ) {
+		return offset;
+	}).offset();
+
+	testElement.innerHTML = "";
+	testElementParent.removeChild( testElement );
+
+	offsetTotal = offset.top + offset.left + ( body ? 2000 : 0 );
+	support.fractions = offsetTotal > 21 && offsetTotal < 22;
+})();
+
+}( jQuery ));

+ 1276 - 0
ambari-web/vendor/scripts/underscore.js

@@ -0,0 +1,1276 @@
+//     Underscore.js 1.5.2
+//     http://underscorejs.org
+//     (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+    // Baseline setup
+    // --------------
+
+    // Establish the root object, `window` in the browser, or `exports` on the server.
+    var root = this;
+
+    // Save the previous value of the `_` variable.
+    var previousUnderscore = root._;
+
+    // Establish the object that gets returned to break out of a loop iteration.
+    var breaker = {};
+
+    // Save bytes in the minified (but not gzipped) version:
+    var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+    // Create quick reference variables for speed access to core prototypes.
+    var
+        push             = ArrayProto.push,
+        slice            = ArrayProto.slice,
+        concat           = ArrayProto.concat,
+        toString         = ObjProto.toString,
+        hasOwnProperty   = ObjProto.hasOwnProperty;
+
+    // All **ECMAScript 5** native function implementations that we hope to use
+    // are declared here.
+    var
+        nativeForEach      = ArrayProto.forEach,
+        nativeMap          = ArrayProto.map,
+        nativeReduce       = ArrayProto.reduce,
+        nativeReduceRight  = ArrayProto.reduceRight,
+        nativeFilter       = ArrayProto.filter,
+        nativeEvery        = ArrayProto.every,
+        nativeSome         = ArrayProto.some,
+        nativeIndexOf      = ArrayProto.indexOf,
+        nativeLastIndexOf  = ArrayProto.lastIndexOf,
+        nativeIsArray      = Array.isArray,
+        nativeKeys         = Object.keys,
+        nativeBind         = FuncProto.bind;
+
+    // Create a safe reference to the Underscore object for use below.
+    var _ = function(obj) {
+        if (obj instanceof _) return obj;
+        if (!(this instanceof _)) return new _(obj);
+        this._wrapped = obj;
+    };
+
+    // Export the Underscore object for **Node.js**, with
+    // backwards-compatibility for the old `require()` API. If we're in
+    // the browser, add `_` as a global object via a string identifier,
+    // for Closure Compiler "advanced" mode.
+    if (typeof exports !== 'undefined') {
+        if (typeof module !== 'undefined' && module.exports) {
+            exports = module.exports = _;
+        }
+        exports._ = _;
+    } else {
+        root._ = _;
+    }
+
+    // Current version.
+    _.VERSION = '1.5.2';
+
+    // Collection Functions
+    // --------------------
+
+    // The cornerstone, an `each` implementation, aka `forEach`.
+    // Handles objects with the built-in `forEach`, arrays, and raw objects.
+    // Delegates to **ECMAScript 5**'s native `forEach` if available.
+    var each = _.each = _.forEach = function(obj, iterator, context) {
+        if (obj == null) return;
+        if (nativeForEach && obj.forEach === nativeForEach) {
+            obj.forEach(iterator, context);
+        } else if (obj.length === +obj.length) {
+            for (var i = 0, length = obj.length; i < length; i++) {
+                if (iterator.call(context, obj[i], i, obj) === breaker) return;
+            }
+        } else {
+            var keys = _.keys(obj);
+            for (var i = 0, length = keys.length; i < length; i++) {
+                if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
+            }
+        }
+    };
+
+    // Return the results of applying the iterator to each element.
+    // Delegates to **ECMAScript 5**'s native `map` if available.
+    _.map = _.collect = function(obj, iterator, context) {
+        var results = [];
+        if (obj == null) return results;
+        if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+        each(obj, function(value, index, list) {
+            results.push(iterator.call(context, value, index, list));
+        });
+        return results;
+    };
+
+    var reduceError = 'Reduce of empty array with no initial value';
+
+    // **Reduce** builds up a single result from a list of values, aka `inject`,
+    // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+    _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+        var initial = arguments.length > 2;
+        if (obj == null) obj = [];
+        if (nativeReduce && obj.reduce === nativeReduce) {
+            if (context) iterator = _.bind(iterator, context);
+            return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+        }
+        each(obj, function(value, index, list) {
+            if (!initial) {
+                memo = value;
+                initial = true;
+            } else {
+                memo = iterator.call(context, memo, value, index, list);
+            }
+        });
+        if (!initial) throw new TypeError(reduceError);
+        return memo;
+    };
+
+    // The right-associative version of reduce, also known as `foldr`.
+    // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+    _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+        var initial = arguments.length > 2;
+        if (obj == null) obj = [];
+        if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+            if (context) iterator = _.bind(iterator, context);
+            return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+        }
+        var length = obj.length;
+        if (length !== +length) {
+            var keys = _.keys(obj);
+            length = keys.length;
+        }
+        each(obj, function(value, index, list) {
+            index = keys ? keys[--length] : --length;
+            if (!initial) {
+                memo = obj[index];
+                initial = true;
+            } else {
+                memo = iterator.call(context, memo, obj[index], index, list);
+            }
+        });
+        if (!initial) throw new TypeError(reduceError);
+        return memo;
+    };
+
+    // Return the first value which passes a truth test. Aliased as `detect`.
+    _.find = _.detect = function(obj, iterator, context) {
+        var result;
+        any(obj, function(value, index, list) {
+            if (iterator.call(context, value, index, list)) {
+                result = value;
+                return true;
+            }
+        });
+        return result;
+    };
+
+    // Return all the elements that pass a truth test.
+    // Delegates to **ECMAScript 5**'s native `filter` if available.
+    // Aliased as `select`.
+    _.filter = _.select = function(obj, iterator, context) {
+        var results = [];
+        if (obj == null) return results;
+        if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+        each(obj, function(value, index, list) {
+            if (iterator.call(context, value, index, list)) results.push(value);
+        });
+        return results;
+    };
+
+    // Return all the elements for which a truth test fails.
+    _.reject = function(obj, iterator, context) {
+        return _.filter(obj, function(value, index, list) {
+            return !iterator.call(context, value, index, list);
+        }, context);
+    };
+
+    // Determine whether all of the elements match a truth test.
+    // Delegates to **ECMAScript 5**'s native `every` if available.
+    // Aliased as `all`.
+    _.every = _.all = function(obj, iterator, context) {
+        iterator || (iterator = _.identity);
+        var result = true;
+        if (obj == null) return result;
+        if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+        each(obj, function(value, index, list) {
+            if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+        });
+        return !!result;
+    };
+
+    // Determine if at least one element in the object matches a truth test.
+    // Delegates to **ECMAScript 5**'s native `some` if available.
+    // Aliased as `any`.
+    var any = _.some = _.any = function(obj, iterator, context) {
+        iterator || (iterator = _.identity);
+        var result = false;
+        if (obj == null) return result;
+        if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+        each(obj, function(value, index, list) {
+            if (result || (result = iterator.call(context, value, index, list))) return breaker;
+        });
+        return !!result;
+    };
+
+    // Determine if the array or object contains a given value (using `===`).
+    // Aliased as `include`.
+    _.contains = _.include = function(obj, target) {
+        if (obj == null) return false;
+        if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+        return any(obj, function(value) {
+            return value === target;
+        });
+    };
+
+    // Invoke a method (with arguments) on every item in a collection.
+    _.invoke = function(obj, method) {
+        var args = slice.call(arguments, 2);
+        var isFunc = _.isFunction(method);
+        return _.map(obj, function(value) {
+            return (isFunc ? method : value[method]).apply(value, args);
+        });
+    };
+
+    // Convenience version of a common use case of `map`: fetching a property.
+    _.pluck = function(obj, key) {
+        return _.map(obj, function(value){ return value[key]; });
+    };
+
+    // Convenience version of a common use case of `filter`: selecting only objects
+    // containing specific `key:value` pairs.
+    _.where = function(obj, attrs, first) {
+        if (_.isEmpty(attrs)) return first ? void 0 : [];
+        return _[first ? 'find' : 'filter'](obj, function(value) {
+            for (var key in attrs) {
+                if (attrs[key] !== value[key]) return false;
+            }
+            return true;
+        });
+    };
+
+    // Convenience version of a common use case of `find`: getting the first object
+    // containing specific `key:value` pairs.
+    _.findWhere = function(obj, attrs) {
+        return _.where(obj, attrs, true);
+    };
+
+    // Return the maximum element or (element-based computation).
+    // Can't optimize arrays of integers longer than 65,535 elements.
+    // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
+    _.max = function(obj, iterator, context) {
+        if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+            return Math.max.apply(Math, obj);
+        }
+        if (!iterator && _.isEmpty(obj)) return -Infinity;
+        var result = {computed : -Infinity, value: -Infinity};
+        each(obj, function(value, index, list) {
+            var computed = iterator ? iterator.call(context, value, index, list) : value;
+            computed > result.computed && (result = {value : value, computed : computed});
+        });
+        return result.value;
+    };
+
+    // Return the minimum element (or element-based computation).
+    _.min = function(obj, iterator, context) {
+        if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+            return Math.min.apply(Math, obj);
+        }
+        if (!iterator && _.isEmpty(obj)) return Infinity;
+        var result = {computed : Infinity, value: Infinity};
+        each(obj, function(value, index, list) {
+            var computed = iterator ? iterator.call(context, value, index, list) : value;
+            computed < result.computed && (result = {value : value, computed : computed});
+        });
+        return result.value;
+    };
+
+    // Shuffle an array, using the modern version of the
+    // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+    _.shuffle = function(obj) {
+        var rand;
+        var index = 0;
+        var shuffled = [];
+        each(obj, function(value) {
+            rand = _.random(index++);
+            shuffled[index - 1] = shuffled[rand];
+            shuffled[rand] = value;
+        });
+        return shuffled;
+    };
+
+    // Sample **n** random values from an array.
+    // If **n** is not specified, returns a single random element from the array.
+    // The internal `guard` argument allows it to work with `map`.
+    _.sample = function(obj, n, guard) {
+        if (arguments.length < 2 || guard) {
+            return obj[_.random(obj.length - 1)];
+        }
+        return _.shuffle(obj).slice(0, Math.max(0, n));
+    };
+
+    // An internal function to generate lookup iterators.
+    var lookupIterator = function(value) {
+        return _.isFunction(value) ? value : function(obj){ return obj[value]; };
+    };
+
+    // Sort the object's values by a criterion produced by an iterator.
+    _.sortBy = function(obj, value, context) {
+        var iterator = lookupIterator(value);
+        return _.pluck(_.map(obj, function(value, index, list) {
+            return {
+                value: value,
+                index: index,
+                criteria: iterator.call(context, value, index, list)
+            };
+        }).sort(function(left, right) {
+            var a = left.criteria;
+            var b = right.criteria;
+            if (a !== b) {
+                if (a > b || a === void 0) return 1;
+                if (a < b || b === void 0) return -1;
+            }
+            return left.index - right.index;
+        }), 'value');
+    };
+
+    // An internal function used for aggregate "group by" operations.
+    var group = function(behavior) {
+        return function(obj, value, context) {
+            var result = {};
+            var iterator = value == null ? _.identity : lookupIterator(value);
+            each(obj, function(value, index) {
+                var key = iterator.call(context, value, index, obj);
+                behavior(result, key, value);
+            });
+            return result;
+        };
+    };
+
+    // Groups the object's values by a criterion. Pass either a string attribute
+    // to group by, or a function that returns the criterion.
+    _.groupBy = group(function(result, key, value) {
+        (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
+    });
+
+    // Indexes the object's values by a criterion, similar to `groupBy`, but for
+    // when you know that your index values will be unique.
+    _.indexBy = group(function(result, key, value) {
+        result[key] = value;
+    });
+
+    // Counts instances of an object that group by a certain criterion. Pass
+    // either a string attribute to count by, or a function that returns the
+    // criterion.
+    _.countBy = group(function(result, key) {
+        _.has(result, key) ? result[key]++ : result[key] = 1;
+    });
+
+    // Use a comparator function to figure out the smallest index at which
+    // an object should be inserted so as to maintain order. Uses binary search.
+    _.sortedIndex = function(array, obj, iterator, context) {
+        iterator = iterator == null ? _.identity : lookupIterator(iterator);
+        var value = iterator.call(context, obj);
+        var low = 0, high = array.length;
+        while (low < high) {
+            var mid = (low + high) >>> 1;
+            iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
+        }
+        return low;
+    };
+
+    // Safely create a real, live array from anything iterable.
+    _.toArray = function(obj) {
+        if (!obj) return [];
+        if (_.isArray(obj)) return slice.call(obj);
+        if (obj.length === +obj.length) return _.map(obj, _.identity);
+        return _.values(obj);
+    };
+
+    // Return the number of elements in an object.
+    _.size = function(obj) {
+        if (obj == null) return 0;
+        return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
+    };
+
+    // Array Functions
+    // ---------------
+
+    // Get the first element of an array. Passing **n** will return the first N
+    // values in the array. Aliased as `head` and `take`. The **guard** check
+    // allows it to work with `_.map`.
+    _.first = _.head = _.take = function(array, n, guard) {
+        if (array == null) return void 0;
+        return (n == null) || guard ? array[0] : slice.call(array, 0, n);
+    };
+
+    // Returns everything but the last entry of the array. Especially useful on
+    // the arguments object. Passing **n** will return all the values in
+    // the array, excluding the last N. The **guard** check allows it to work with
+    // `_.map`.
+    _.initial = function(array, n, guard) {
+        return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
+    };
+
+    // Get the last element of an array. Passing **n** will return the last N
+    // values in the array. The **guard** check allows it to work with `_.map`.
+    _.last = function(array, n, guard) {
+        if (array == null) return void 0;
+        if ((n == null) || guard) {
+            return array[array.length - 1];
+        } else {
+            return slice.call(array, Math.max(array.length - n, 0));
+        }
+    };
+
+    // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+    // Especially useful on the arguments object. Passing an **n** will return
+    // the rest N values in the array. The **guard**
+    // check allows it to work with `_.map`.
+    _.rest = _.tail = _.drop = function(array, n, guard) {
+        return slice.call(array, (n == null) || guard ? 1 : n);
+    };
+
+    // Trim out all falsy values from an array.
+    _.compact = function(array) {
+        return _.filter(array, _.identity);
+    };
+
+    // Internal implementation of a recursive `flatten` function.
+    var flatten = function(input, shallow, output) {
+        if (shallow && _.every(input, _.isArray)) {
+            return concat.apply(output, input);
+        }
+        each(input, function(value) {
+            if (_.isArray(value) || _.isArguments(value)) {
+                shallow ? push.apply(output, value) : flatten(value, shallow, output);
+            } else {
+                output.push(value);
+            }
+        });
+        return output;
+    };
+
+    // Flatten out an array, either recursively (by default), or just one level.
+    _.flatten = function(array, shallow) {
+        return flatten(array, shallow, []);
+    };
+
+    // Return a version of the array that does not contain the specified value(s).
+    _.without = function(array) {
+        return _.difference(array, slice.call(arguments, 1));
+    };
+
+    // Produce a duplicate-free version of the array. If the array has already
+    // been sorted, you have the option of using a faster algorithm.
+    // Aliased as `unique`.
+    _.uniq = _.unique = function(array, isSorted, iterator, context) {
+        if (_.isFunction(isSorted)) {
+            context = iterator;
+            iterator = isSorted;
+            isSorted = false;
+        }
+        var initial = iterator ? _.map(array, iterator, context) : array;
+        var results = [];
+        var seen = [];
+        each(initial, function(value, index) {
+            if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
+                seen.push(value);
+                results.push(array[index]);
+            }
+        });
+        return results;
+    };
+
+    // Produce an array that contains the union: each distinct element from all of
+    // the passed-in arrays.
+    _.union = function() {
+        return _.uniq(_.flatten(arguments, true));
+    };
+
+    // Produce an array that contains every item shared between all the
+    // passed-in arrays.
+    _.intersection = function(array) {
+        var rest = slice.call(arguments, 1);
+        return _.filter(_.uniq(array), function(item) {
+            return _.every(rest, function(other) {
+                return _.indexOf(other, item) >= 0;
+            });
+        });
+    };
+
+    // Take the difference between one array and a number of other arrays.
+    // Only the elements present in just the first array will remain.
+    _.difference = function(array) {
+        var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
+        return _.filter(array, function(value){ return !_.contains(rest, value); });
+    };
+
+    // Zip together multiple lists into a single array -- elements that share
+    // an index go together.
+    _.zip = function() {
+        var length = _.max(_.pluck(arguments, "length").concat(0));
+        var results = new Array(length);
+        for (var i = 0; i < length; i++) {
+            results[i] = _.pluck(arguments, '' + i);
+        }
+        return results;
+    };
+
+    // Converts lists into objects. Pass either a single array of `[key, value]`
+    // pairs, or two parallel arrays of the same length -- one of keys, and one of
+    // the corresponding values.
+    _.object = function(list, values) {
+        if (list == null) return {};
+        var result = {};
+        for (var i = 0, length = list.length; i < length; i++) {
+            if (values) {
+                result[list[i]] = values[i];
+            } else {
+                result[list[i][0]] = list[i][1];
+            }
+        }
+        return result;
+    };
+
+    // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+    // we need this function. Return the position of the first occurrence of an
+    // item in an array, or -1 if the item is not included in the array.
+    // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+    // If the array is large and already in sort order, pass `true`
+    // for **isSorted** to use binary search.
+    _.indexOf = function(array, item, isSorted) {
+        if (array == null) return -1;
+        var i = 0, length = array.length;
+        if (isSorted) {
+            if (typeof isSorted == 'number') {
+                i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
+            } else {
+                i = _.sortedIndex(array, item);
+                return array[i] === item ? i : -1;
+            }
+        }
+        if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
+        for (; i < length; i++) if (array[i] === item) return i;
+        return -1;
+    };
+
+    // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+    _.lastIndexOf = function(array, item, from) {
+        if (array == null) return -1;
+        var hasIndex = from != null;
+        if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
+            return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
+        }
+        var i = (hasIndex ? from : array.length);
+        while (i--) if (array[i] === item) return i;
+        return -1;
+    };
+
+    // Generate an integer Array containing an arithmetic progression. A port of
+    // the native Python `range()` function. See
+    // [the Python documentation](http://docs.python.org/library/functions.html#range).
+    _.range = function(start, stop, step) {
+        if (arguments.length <= 1) {
+            stop = start || 0;
+            start = 0;
+        }
+        step = arguments[2] || 1;
+
+        var length = Math.max(Math.ceil((stop - start) / step), 0);
+        var idx = 0;
+        var range = new Array(length);
+
+        while(idx < length) {
+            range[idx++] = start;
+            start += step;
+        }
+
+        return range;
+    };
+
+    // Function (ahem) Functions
+    // ------------------
+
+    // Reusable constructor function for prototype setting.
+    var ctor = function(){};
+
+    // Create a function bound to a given object (assigning `this`, and arguments,
+    // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+    // available.
+    _.bind = function(func, context) {
+        var args, bound;
+        if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+        if (!_.isFunction(func)) throw new TypeError;
+        args = slice.call(arguments, 2);
+        return bound = function() {
+            if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
+            ctor.prototype = func.prototype;
+            var self = new ctor;
+            ctor.prototype = null;
+            var result = func.apply(self, args.concat(slice.call(arguments)));
+            if (Object(result) === result) return result;
+            return self;
+        };
+    };
+
+    // Partially apply a function by creating a version that has had some of its
+    // arguments pre-filled, without changing its dynamic `this` context.
+    _.partial = function(func) {
+        var args = slice.call(arguments, 1);
+        return function() {
+            return func.apply(this, args.concat(slice.call(arguments)));
+        };
+    };
+
+    // Bind all of an object's methods to that object. Useful for ensuring that
+    // all callbacks defined on an object belong to it.
+    _.bindAll = function(obj) {
+        var funcs = slice.call(arguments, 1);
+        if (funcs.length === 0) throw new Error("bindAll must be passed function names");
+        each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+        return obj;
+    };
+
+    // Memoize an expensive function by storing its results.
+    _.memoize = function(func, hasher) {
+        var memo = {};
+        hasher || (hasher = _.identity);
+        return function() {
+            var key = hasher.apply(this, arguments);
+            return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+        };
+    };
+
+    // Delays a function for the given number of milliseconds, and then calls
+    // it with the arguments supplied.
+    _.delay = function(func, wait) {
+        var args = slice.call(arguments, 2);
+        return setTimeout(function(){ return func.apply(null, args); }, wait);
+    };
+
+    // Defers a function, scheduling it to run after the current call stack has
+    // cleared.
+    _.defer = function(func) {
+        return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+    };
+
+    // Returns a function, that, when invoked, will only be triggered at most once
+    // during a given window of time. Normally, the throttled function will run
+    // as much as it can, without ever going more than once per `wait` duration;
+    // but if you'd like to disable the execution on the leading edge, pass
+    // `{leading: false}`. To disable execution on the trailing edge, ditto.
+    _.throttle = function(func, wait, options) {
+        var context, args, result;
+        var timeout = null;
+        var previous = 0;
+        options || (options = {});
+        var later = function() {
+            previous = options.leading === false ? 0 : new Date;
+            timeout = null;
+            result = func.apply(context, args);
+        };
+        return function() {
+            var now = new Date;
+            if (!previous && options.leading === false) previous = now;
+            var remaining = wait - (now - previous);
+            context = this;
+            args = arguments;
+            if (remaining <= 0) {
+                clearTimeout(timeout);
+                timeout = null;
+                previous = now;
+                result = func.apply(context, args);
+            } else if (!timeout && options.trailing !== false) {
+                timeout = setTimeout(later, remaining);
+            }
+            return result;
+        };
+    };
+
+    // Returns a function, that, as long as it continues to be invoked, will not
+    // be triggered. The function will be called after it stops being called for
+    // N milliseconds. If `immediate` is passed, trigger the function on the
+    // leading edge, instead of the trailing.
+    _.debounce = function(func, wait, immediate) {
+        var timeout, args, context, timestamp, result;
+        return function() {
+            context = this;
+            args = arguments;
+            timestamp = new Date();
+            var later = function() {
+                var last = (new Date()) - timestamp;
+                if (last < wait) {
+                    timeout = setTimeout(later, wait - last);
+                } else {
+                    timeout = null;
+                    if (!immediate) result = func.apply(context, args);
+                }
+            };
+            var callNow = immediate && !timeout;
+            if (!timeout) {
+                timeout = setTimeout(later, wait);
+            }
+            if (callNow) result = func.apply(context, args);
+            return result;
+        };
+    };
+
+    // Returns a function that will be executed at most one time, no matter how
+    // often you call it. Useful for lazy initialization.
+    _.once = function(func) {
+        var ran = false, memo;
+        return function() {
+            if (ran) return memo;
+            ran = true;
+            memo = func.apply(this, arguments);
+            func = null;
+            return memo;
+        };
+    };
+
+    // Returns the first function passed as an argument to the second,
+    // allowing you to adjust arguments, run code before and after, and
+    // conditionally execute the original function.
+    _.wrap = function(func, wrapper) {
+        return function() {
+            var args = [func];
+            push.apply(args, arguments);
+            return wrapper.apply(this, args);
+        };
+    };
+
+    // Returns a function that is the composition of a list of functions, each
+    // consuming the return value of the function that follows.
+    _.compose = function() {
+        var funcs = arguments;
+        return function() {
+            var args = arguments;
+            for (var i = funcs.length - 1; i >= 0; i--) {
+                args = [funcs[i].apply(this, args)];
+            }
+            return args[0];
+        };
+    };
+
+    // Returns a function that will only be executed after being called N times.
+    _.after = function(times, func) {
+        return function() {
+            if (--times < 1) {
+                return func.apply(this, arguments);
+            }
+        };
+    };
+
+    // Object Functions
+    // ----------------
+
+    // Retrieve the names of an object's properties.
+    // Delegates to **ECMAScript 5**'s native `Object.keys`
+    _.keys = nativeKeys || function(obj) {
+        if (obj !== Object(obj)) throw new TypeError('Invalid object');
+        var keys = [];
+        for (var key in obj) if (_.has(obj, key)) keys.push(key);
+        return keys;
+    };
+
+    // Retrieve the values of an object's properties.
+    _.values = function(obj) {
+        var keys = _.keys(obj);
+        var length = keys.length;
+        var values = new Array(length);
+        for (var i = 0; i < length; i++) {
+            values[i] = obj[keys[i]];
+        }
+        return values;
+    };
+
+    // Convert an object into a list of `[key, value]` pairs.
+    _.pairs = function(obj) {
+        var keys = _.keys(obj);
+        var length = keys.length;
+        var pairs = new Array(length);
+        for (var i = 0; i < length; i++) {
+            pairs[i] = [keys[i], obj[keys[i]]];
+        }
+        return pairs;
+    };
+
+    // Invert the keys and values of an object. The values must be serializable.
+    _.invert = function(obj) {
+        var result = {};
+        var keys = _.keys(obj);
+        for (var i = 0, length = keys.length; i < length; i++) {
+            result[obj[keys[i]]] = keys[i];
+        }
+        return result;
+    };
+
+    // Return a sorted list of the function names available on the object.
+    // Aliased as `methods`
+    _.functions = _.methods = function(obj) {
+        var names = [];
+        for (var key in obj) {
+            if (_.isFunction(obj[key])) names.push(key);
+        }
+        return names.sort();
+    };
+
+    // Extend a given object with all the properties in passed-in object(s).
+    _.extend = function(obj) {
+        each(slice.call(arguments, 1), function(source) {
+            if (source) {
+                for (var prop in source) {
+                    obj[prop] = source[prop];
+                }
+            }
+        });
+        return obj;
+    };
+
+    // Return a copy of the object only containing the whitelisted properties.
+    _.pick = function(obj) {
+        var copy = {};
+        var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+        each(keys, function(key) {
+            if (key in obj) copy[key] = obj[key];
+        });
+        return copy;
+    };
+
+    // Return a copy of the object without the blacklisted properties.
+    _.omit = function(obj) {
+        var copy = {};
+        var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+        for (var key in obj) {
+            if (!_.contains(keys, key)) copy[key] = obj[key];
+        }
+        return copy;
+    };
+
+    // Fill in a given object with default properties.
+    _.defaults = function(obj) {
+        each(slice.call(arguments, 1), function(source) {
+            if (source) {
+                for (var prop in source) {
+                    if (obj[prop] === void 0) obj[prop] = source[prop];
+                }
+            }
+        });
+        return obj;
+    };
+
+    // Create a (shallow-cloned) duplicate of an object.
+    _.clone = function(obj) {
+        if (!_.isObject(obj)) return obj;
+        return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+    };
+
+    // Invokes interceptor with the obj, and then returns obj.
+    // The primary purpose of this method is to "tap into" a method chain, in
+    // order to perform operations on intermediate results within the chain.
+    _.tap = function(obj, interceptor) {
+        interceptor(obj);
+        return obj;
+    };
+
+    // Internal recursive comparison function for `isEqual`.
+    var eq = function(a, b, aStack, bStack) {
+        // Identical objects are equal. `0 === -0`, but they aren't identical.
+        // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+        if (a === b) return a !== 0 || 1 / a == 1 / b;
+        // A strict comparison is necessary because `null == undefined`.
+        if (a == null || b == null) return a === b;
+        // Unwrap any wrapped objects.
+        if (a instanceof _) a = a._wrapped;
+        if (b instanceof _) b = b._wrapped;
+        // Compare `[[Class]]` names.
+        var className = toString.call(a);
+        if (className != toString.call(b)) return false;
+        switch (className) {
+            // Strings, numbers, dates, and booleans are compared by value.
+            case '[object String]':
+                // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+                // equivalent to `new String("5")`.
+                return a == String(b);
+            case '[object Number]':
+                // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+                // other numeric values.
+                return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
+            case '[object Date]':
+            case '[object Boolean]':
+                // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+                // millisecond representations. Note that invalid dates with millisecond representations
+                // of `NaN` are not equivalent.
+                return +a == +b;
+            // RegExps are compared by their source patterns and flags.
+            case '[object RegExp]':
+                return a.source == b.source &&
+                    a.global == b.global &&
+                    a.multiline == b.multiline &&
+                    a.ignoreCase == b.ignoreCase;
+        }
+        if (typeof a != 'object' || typeof b != 'object') return false;
+        // Assume equality for cyclic structures. The algorithm for detecting cyclic
+        // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+        var length = aStack.length;
+        while (length--) {
+            // Linear search. Performance is inversely proportional to the number of
+            // unique nested structures.
+            if (aStack[length] == a) return bStack[length] == b;
+        }
+        // Objects with different constructors are not equivalent, but `Object`s
+        // from different frames are.
+        var aCtor = a.constructor, bCtor = b.constructor;
+        if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
+            _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
+            return false;
+        }
+        // Add the first object to the stack of traversed objects.
+        aStack.push(a);
+        bStack.push(b);
+        var size = 0, result = true;
+        // Recursively compare objects and arrays.
+        if (className == '[object Array]') {
+            // Compare array lengths to determine if a deep comparison is necessary.
+            size = a.length;
+            result = size == b.length;
+            if (result) {
+                // Deep compare the contents, ignoring non-numeric properties.
+                while (size--) {
+                    if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+                }
+            }
+        } else {
+            // Deep compare objects.
+            for (var key in a) {
+                if (_.has(a, key)) {
+                    // Count the expected number of properties.
+                    size++;
+                    // Deep compare each member.
+                    if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+                }
+            }
+            // Ensure that both objects contain the same number of properties.
+            if (result) {
+                for (key in b) {
+                    if (_.has(b, key) && !(size--)) break;
+                }
+                result = !size;
+            }
+        }
+        // Remove the first object from the stack of traversed objects.
+        aStack.pop();
+        bStack.pop();
+        return result;
+    };
+
+    // Perform a deep comparison to check if two objects are equal.
+    _.isEqual = function(a, b) {
+        return eq(a, b, [], []);
+    };
+
+    // Is a given array, string, or object empty?
+    // An "empty" object has no enumerable own-properties.
+    _.isEmpty = function(obj) {
+        if (obj == null) return true;
+        if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+        for (var key in obj) if (_.has(obj, key)) return false;
+        return true;
+    };
+
+    // Is a given value a DOM element?
+    _.isElement = function(obj) {
+        return !!(obj && obj.nodeType === 1);
+    };
+
+    // Is a given value an array?
+    // Delegates to ECMA5's native Array.isArray
+    _.isArray = nativeIsArray || function(obj) {
+        return toString.call(obj) == '[object Array]';
+    };
+
+    // Is a given variable an object?
+    _.isObject = function(obj) {
+        return obj === Object(obj);
+    };
+
+    // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+    each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+        _['is' + name] = function(obj) {
+            return toString.call(obj) == '[object ' + name + ']';
+        };
+    });
+
+    // Define a fallback version of the method in browsers (ahem, IE), where
+    // there isn't any inspectable "Arguments" type.
+    if (!_.isArguments(arguments)) {
+        _.isArguments = function(obj) {
+            return !!(obj && _.has(obj, 'callee'));
+        };
+    }
+
+    // Optimize `isFunction` if appropriate.
+    if (typeof (/./) !== 'function') {
+        _.isFunction = function(obj) {
+            return typeof obj === 'function';
+        };
+    }
+
+    // Is a given object a finite number?
+    _.isFinite = function(obj) {
+        return isFinite(obj) && !isNaN(parseFloat(obj));
+    };
+
+    // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+    _.isNaN = function(obj) {
+        return _.isNumber(obj) && obj != +obj;
+    };
+
+    // Is a given value a boolean?
+    _.isBoolean = function(obj) {
+        return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
+    };
+
+    // Is a given value equal to null?
+    _.isNull = function(obj) {
+        return obj === null;
+    };
+
+    // Is a given variable undefined?
+    _.isUndefined = function(obj) {
+        return obj === void 0;
+    };
+
+    // Shortcut function for checking if an object has a given property directly
+    // on itself (in other words, not on a prototype).
+    _.has = function(obj, key) {
+        return hasOwnProperty.call(obj, key);
+    };
+
+    // Utility Functions
+    // -----------------
+
+    // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+    // previous owner. Returns a reference to the Underscore object.
+    _.noConflict = function() {
+        root._ = previousUnderscore;
+        return this;
+    };
+
+    // Keep the identity function around for default iterators.
+    _.identity = function(value) {
+        return value;
+    };
+
+    // Run a function **n** times.
+    _.times = function(n, iterator, context) {
+        var accum = Array(Math.max(0, n));
+        for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
+        return accum;
+    };
+
+    // Return a random integer between min and max (inclusive).
+    _.random = function(min, max) {
+        if (max == null) {
+            max = min;
+            min = 0;
+        }
+        return min + Math.floor(Math.random() * (max - min + 1));
+    };
+
+    // List of HTML entities for escaping.
+    var entityMap = {
+        escape: {
+            '&': '&amp;',
+            '<': '&lt;',
+            '>': '&gt;',
+            '"': '&quot;',
+            "'": '&#x27;'
+        }
+    };
+    entityMap.unescape = _.invert(entityMap.escape);
+
+    // Regexes containing the keys and values listed immediately above.
+    var entityRegexes = {
+        escape:   new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
+        unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
+    };
+
+    // Functions for escaping and unescaping strings to/from HTML interpolation.
+    _.each(['escape', 'unescape'], function(method) {
+        _[method] = function(string) {
+            if (string == null) return '';
+            return ('' + string).replace(entityRegexes[method], function(match) {
+                return entityMap[method][match];
+            });
+        };
+    });
+
+    // If the value of the named `property` is a function then invoke it with the
+    // `object` as context; otherwise, return it.
+    _.result = function(object, property) {
+        if (object == null) return void 0;
+        var value = object[property];
+        return _.isFunction(value) ? value.call(object) : value;
+    };
+
+    // Add your own custom functions to the Underscore object.
+    _.mixin = function(obj) {
+        each(_.functions(obj), function(name) {
+            var func = _[name] = obj[name];
+            _.prototype[name] = function() {
+                var args = [this._wrapped];
+                push.apply(args, arguments);
+                return result.call(this, func.apply(_, args));
+            };
+        });
+    };
+
+    // Generate a unique integer id (unique within the entire client session).
+    // Useful for temporary DOM ids.
+    var idCounter = 0;
+    _.uniqueId = function(prefix) {
+        var id = ++idCounter + '';
+        return prefix ? prefix + id : id;
+    };
+
+    // By default, Underscore uses ERB-style template delimiters, change the
+    // following template settings to use alternative delimiters.
+    _.templateSettings = {
+        evaluate    : /<%([\s\S]+?)%>/g,
+        interpolate : /<%=([\s\S]+?)%>/g,
+        escape      : /<%-([\s\S]+?)%>/g
+    };
+
+    // When customizing `templateSettings`, if you don't want to define an
+    // interpolation, evaluation or escaping regex, we need one that is
+    // guaranteed not to match.
+    var noMatch = /(.)^/;
+
+    // Certain characters need to be escaped so that they can be put into a
+    // string literal.
+    var escapes = {
+        "'":      "'",
+        '\\':     '\\',
+        '\r':     'r',
+        '\n':     'n',
+        '\t':     't',
+        '\u2028': 'u2028',
+        '\u2029': 'u2029'
+    };
+
+    var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+
+    // JavaScript micro-templating, similar to John Resig's implementation.
+    // Underscore templating handles arbitrary delimiters, preserves whitespace,
+    // and correctly escapes quotes within interpolated code.
+    _.template = function(text, data, settings) {
+        var render;
+        settings = _.defaults({}, settings, _.templateSettings);
+
+        // Combine delimiters into one regular expression via alternation.
+        var matcher = new RegExp([
+            (settings.escape || noMatch).source,
+            (settings.interpolate || noMatch).source,
+            (settings.evaluate || noMatch).source
+        ].join('|') + '|$', 'g');
+
+        // Compile the template source, escaping string literals appropriately.
+        var index = 0;
+        var source = "__p+='";
+        text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+            source += text.slice(index, offset)
+                .replace(escaper, function(match) { return '\\' + escapes[match]; });
+
+            if (escape) {
+                source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+            }
+            if (interpolate) {
+                source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+            }
+            if (evaluate) {
+                source += "';\n" + evaluate + "\n__p+='";
+            }
+            index = offset + match.length;
+            return match;
+        });
+        source += "';\n";
+
+        // If a variable is not specified, place data values in local scope.
+        if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+        source = "var __t,__p='',__j=Array.prototype.join," +
+        "print=function(){__p+=__j.call(arguments,'');};\n" +
+        source + "return __p;\n";
+
+        try {
+            render = new Function(settings.variable || 'obj', '_', source);
+        } catch (e) {
+            e.source = source;
+            throw e;
+        }
+
+        if (data) return render(data, _);
+        var template = function(data) {
+            return render.call(this, data, _);
+        };
+
+        // Provide the compiled function source as a convenience for precompilation.
+        template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+
+        return template;
+    };
+
+    // Add a "chain" function, which will delegate to the wrapper.
+    _.chain = function(obj) {
+        return _(obj).chain();
+    };
+
+    // OOP
+    // ---------------
+    // If Underscore is called as a function, it returns a wrapped object that
+    // can be used OO-style. This wrapper holds altered versions of all the
+    // underscore functions. Wrapped objects may be chained.
+
+    // Helper function to continue chaining intermediate results.
+    var result = function(obj) {
+        return this._chain ? _(obj).chain() : obj;
+    };
+
+    // Add all of the Underscore functions to the wrapper object.
+    _.mixin(_);
+
+    // Add all mutator Array functions to the wrapper.
+    each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+        var method = ArrayProto[name];
+        _.prototype[name] = function() {
+            var obj = this._wrapped;
+            method.apply(obj, arguments);
+            if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
+            return result.call(this, obj);
+        };
+    });
+
+    // Add all accessor Array functions to the wrapper.
+    each(['concat', 'join', 'slice'], function(name) {
+        var method = ArrayProto[name];
+        _.prototype[name] = function() {
+            return result.call(this, method.apply(this._wrapped, arguments));
+        };
+    });
+
+    _.extend(_.prototype, {
+
+        // Start chaining a wrapped Underscore object.
+        chain: function() {
+            this._chain = true;
+            return this;
+        },
+
+        // Extracts the result from a wrapped and chained object.
+        value: function() {
+            return this._wrapped;
+        }
+
+    });
+
+}).call(this);

+ 1969 - 0
ambari-web/vendor/scripts/visualsearch.js

@@ -0,0 +1,1969 @@
+// This is the annotated source code for
+// [VisualSearch.js](http://documentcloud.github.com/visualsearch/),
+// a rich search box for real data.
+//
+// The annotated source HTML is generated by
+// [Docco](http://jashkenas.github.com/docco/).
+
+/** @license VisualSearch.js 0.4.0
+ *  (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc.
+ *  VisualSearch.js may be freely distributed under the MIT license.
+ *  For all details and documentation:
+ *  http://documentcloud.github.com/visualsearch
+ */
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+    // Setting up VisualSearch globals. These will eventually be made instance-based.
+    if (!window.VS) window.VS = {};
+    if (!VS.app)    VS.app    = {};
+    if (!VS.ui)     VS.ui     = {};
+    if (!VS.model)  VS.model  = {};
+    if (!VS.utils)  VS.utils  = {};
+
+    // Sets the version for VisualSearch to be used programatically elsewhere.
+    VS.VERSION = '0.5.0';
+
+    VS.VisualSearch = function(options) {
+        var defaults = {
+            container   : '',
+            query       : '',
+            autosearch  : true,
+            unquotable  : [],
+            remainder   : 'text',
+            showFacets  : true,
+            readOnly    : false,
+            callbacks   : {
+                search          : $.noop,
+                focus           : $.noop,
+                blur            : $.noop,
+                facetMatches    : $.noop,
+                valueMatches    : $.noop,
+                clearSearch     : $.noop,
+                removedFacet    : $.noop
+            }
+        };
+        this.options           = _.extend({}, defaults, options);
+        this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks);
+
+        VS.app.hotkeys.initialize();
+        this.searchQuery   = new VS.model.SearchQuery();
+        this.searchBox     = new VS.ui.SearchBox({
+            app: this,
+            showFacets: this.options.showFacets
+        });
+
+        if (options.container) {
+            var searchBox = this.searchBox.render().el;
+            $(this.options.container).html(searchBox);
+        }
+        this.searchBox.value(this.options.query || '');
+
+        // Disable page caching for browsers that incorrectly cache the visual search inputs.
+        // This forces the browser to re-render the page when it is retrieved in its history.
+        $(window).bind('unload', function(e) {});
+
+        // Gives the user back a reference to the `searchBox` so they
+        // can use public methods.
+        return this;
+    };
+
+    // Entry-point used to tie all parts of VisualSearch together. It will either attach
+    // itself to `options.container`, or pass back the `searchBox` so it can be rendered
+    // at will.
+    VS.init = function(options) {
+        return new VS.VisualSearch(options);
+    };
+
+})();
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// The search box is responsible for managing the many facet views and input views.
+    VS.ui.SearchBox = Backbone.View.extend({
+
+        id  : 'search',
+
+        events : {
+            'click .VS-cancel-search-box' : 'clearSearch',
+            'mousedown .VS-search-box'    : 'maybeFocusSearch',
+            'dblclick .VS-search-box'     : 'highlightSearch',
+            'click .VS-search-box'        : 'maybeTripleClick'
+        },
+
+        // Creating a new SearchBox registers handlers for re-rendering facets when necessary,
+        // as well as handling typing when a facet is selected.
+        initialize : function(options) {
+            this.options = _.extend({}, this.options, options);
+
+            this.app = this.options.app;
+            this.flags = {
+                allSelected : false
+            };
+            this.facetViews = [];
+            this.inputViews = [];
+            _.bindAll(this, 'renderFacets', '_maybeDisableFacets', 'disableFacets',
+                'deselectAllFacets', 'addedFacet', 'removedFacet', 'changedFacet');
+            this.app.searchQuery
+                .bind('reset', this.renderFacets)
+                .bind('add', this.addedFacet)
+                .bind('remove', this.removedFacet)
+                .bind('change', this.changedFacet);
+            $(document).bind('keydown', this._maybeDisableFacets);
+        },
+
+        // Renders the search box, but requires placement on the page through `this.el`.
+        render : function() {
+            $(this.el).append(JST['search_box']({
+                readOnly: this.app.options.readOnly
+            }));
+            $(document.body).setMode('no', 'search');
+
+            return this;
+        },
+
+        // # Querying Facets #
+
+        // Either gets a serialized query string or sets the faceted query from a query string.
+        value : function(query) {
+            if (query == null) return this.serialize();
+            return this.setQuery(query);
+        },
+
+        // Uses the VS.app.searchQuery collection to serialize the current query from the various
+        // facets that are in the search box.
+        serialize : function() {
+            var query           = [];
+            var inputViewsCount = this.inputViews.length;
+
+            this.app.searchQuery.each(_.bind(function(facet, i) {
+                query.push(this.inputViews[i].value());
+                query.push(facet.serialize());
+            }, this));
+
+            if (inputViewsCount) {
+                query.push(this.inputViews[inputViewsCount-1].value());
+            }
+
+            return _.compact(query).join(' ');
+        },
+
+        // Returns any facet views that are currently selected. Useful for changing the value
+        // callbacks based on what else is in the search box and which facet is being edited.
+        selected: function() {
+            return _.select(this.facetViews, function(view) {
+                return view.modes.editing == 'is' || view.modes.selected == 'is';
+            });
+        },
+
+        // Similar to `this.selected`, returns any facet models that are currently selected.
+        selectedModels: function() {
+            return _.pluck(this.selected(), 'model');
+        },
+
+        // Takes a query string and uses the SearchParser to parse and render it. Note that
+        // `VS.app.SearchParser` refreshes the `VS.app.searchQuery` collection, which is bound
+        // here to call `this.renderFacets`.
+        setQuery : function(query) {
+            this.currentQuery = query;
+            VS.app.SearchParser.parse(this.app, query);
+        },
+
+        // Returns the position of a facet/input view. Useful when moving between facets.
+        viewPosition : function(view) {
+            var views    = view.type == 'facet' ? this.facetViews : this.inputViews;
+            var position = _.indexOf(views, view);
+            if (position == -1) position = 0;
+            return position;
+        },
+
+        // Used to launch a search. Hitting enter or clicking the search button.
+        searchEvent : function(e) {
+            var query = this.value();
+            this.focusSearch(e);
+            this.value(query);
+            this.app.options.callbacks.search(query, this.app.searchQuery);
+        },
+
+        // # Rendering Facets #
+
+        // Add a new facet. Facet will be focused and ready to accept a value. Can also
+        // specify position, in the case of adding facets from an inbetween input.
+        addFacet : function(category, initialQuery, position) {
+            category     = VS.utils.inflector.trim(category);
+            initialQuery = VS.utils.inflector.trim(initialQuery || '');
+            if (!category) return;
+
+            var model = new VS.model.SearchFacet({
+                category : category,
+                value    : initialQuery || '',
+                app      : this.app
+            });
+            this.app.searchQuery.add(model, {at: position});
+        },
+
+        // Renders a newly added facet, and selects it.
+        addedFacet : function (model) {
+            this.renderFacets();
+            var facetView = _.detect(this.facetViews, function(view) {
+                if (view.model == model) return true;
+            });
+
+            _.defer(function() {
+                facetView.enableEdit();
+            });
+        },
+
+        // Changing a facet programmatically re-renders it.
+        changedFacet: function () {
+            this.renderFacets();
+        },
+
+        // When removing a facet, potentially do something. For now, the adjacent
+        // remaining facet is selected, but this is handled by the facet's view,
+        // since its position is unknown by the time the collection triggers this
+        // remove callback.
+        removedFacet : function (facet, query, options) {
+            this.app.options.callbacks.removedFacet(facet, query, options);
+        },
+
+        // Renders each facet as a searchFacet view.
+        renderFacets : function() {
+            this.facetViews = [];
+            this.inputViews = [];
+
+            this.$('.VS-search-inner').empty();
+
+            this.app.searchQuery.each(_.bind(this.renderFacet, this));
+
+            // Add on an n+1 empty search input on the very end.
+            this.renderSearchInput();
+            this.renderPlaceholder();
+        },
+
+        // Render a single facet, using its category and query value.
+        renderFacet : function(facet, position) {
+            var view = new VS.ui.SearchFacet({
+                app   : this.app,
+                model : facet,
+                order : position
+            });
+
+            // Input first, facet second.
+            this.renderSearchInput();
+            this.facetViews.push(view);
+            this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
+
+            view.calculateSize();
+            _.defer(_.bind(view.calculateSize, view));
+
+            return view;
+        },
+
+        // Render a single input, used to create and autocomplete facets
+        renderSearchInput : function() {
+            var input = new VS.ui.SearchInput({
+                position: this.inputViews.length,
+                app: this.app,
+                showFacets: this.options.showFacets
+            });
+            this.$('.VS-search-inner').append(input.render().el);
+            this.inputViews.push(input);
+        },
+
+        // Handles showing/hiding the placeholder text
+        renderPlaceholder : function() {
+            var $placeholder = this.$('.VS-placeholder');
+            if (this.app.searchQuery.length) {
+                $placeholder.addClass("VS-hidden");
+            } else {
+                $placeholder.removeClass("VS-hidden")
+                    .text(this.app.options.placeholder);
+            }
+        },
+
+        // # Modifying Facets #
+
+        // Clears out the search box. Command+A + delete can trigger this, as can a cancel button.
+        //
+        // If a `clearSearch` callback was provided, the callback is invoked and
+        // provided with a function performs the actual removal of the data.  This
+        // allows third-party developers to either clear data asynchronously, or
+        // prior to performing their custom "clear" logic.
+        clearSearch : function(e) {
+            if (this.app.options.readOnly) return;
+            var actualClearSearch = _.bind(function() {
+                this.disableFacets();
+                this.value('');
+                this.flags.allSelected = false;
+                this.searchEvent(e);
+                this.focusSearch(e);
+            }, this);
+
+            if (this.app.options.callbacks.clearSearch != $.noop) {
+                this.app.options.callbacks.clearSearch(actualClearSearch);
+            } else {
+                actualClearSearch();
+            }
+        },
+
+        // Command+A selects all facets.
+        selectAllFacets : function() {
+            this.flags.allSelected = true;
+
+            $(document).one('click.selectAllFacets', this.deselectAllFacets);
+
+            _.each(this.facetViews, function(facetView, i) {
+                facetView.selectFacet();
+            });
+            _.each(this.inputViews, function(inputView, i) {
+                inputView.selectText();
+            });
+        },
+
+        // Used by facets and input to see if all facets are currently selected.
+        allSelected : function(deselect) {
+            if (deselect) this.flags.allSelected = false;
+            return this.flags.allSelected;
+        },
+
+        // After `selectAllFacets` is engaged, this method is bound to the entire document.
+        // This immediate disables and deselects all facets, but it also checks if the user
+        // has clicked on either a facet or an input, and properly selects the view.
+        deselectAllFacets : function(e) {
+            this.disableFacets();
+
+            if (this.$(e.target).is('.category,input')) {
+                var el   = $(e.target).closest('.search_facet,.search_input');
+                var view = _.detect(this.facetViews.concat(this.inputViews), function(v) {
+                    return v.el == el[0];
+                });
+                if (view.type == 'facet') {
+                    view.selectFacet();
+                } else if (view.type == 'input') {
+                    _.defer(function() {
+                        view.enableEdit(true);
+                    });
+                }
+            }
+        },
+
+        // Disables all facets except for the passed in view. Used when switching between
+        // facets, so as not to have to keep state of active facets.
+        disableFacets : function(keepView) {
+            _.each(this.inputViews, function(view) {
+                if (view && view != keepView &&
+                    (view.modes.editing == 'is' || view.modes.selected == 'is')) {
+                    view.disableEdit();
+                }
+            });
+            _.each(this.facetViews, function(view) {
+                if (view && view != keepView &&
+                    (view.modes.editing == 'is' || view.modes.selected == 'is')) {
+                    view.disableEdit();
+                    view.deselectFacet();
+                }
+            });
+
+            this.flags.allSelected = false;
+            this.removeFocus();
+            $(document).unbind('click.selectAllFacets');
+        },
+
+        // Resize all inputs to account for extra keystrokes which may be changing the facet
+        // width incorrectly. This is a safety check to ensure inputs are correctly sized.
+        resizeFacets : function(view) {
+            _.each(this.facetViews, function(facetView, i) {
+                if (!view || facetView == view) {
+                    facetView.resize();
+                }
+            });
+        },
+
+        // Handles keydown events on the document. Used to complete the Cmd+A deletion, and
+        // blurring focus.
+        _maybeDisableFacets : function(e) {
+            if (this.flags.allSelected && VS.app.hotkeys.key(e) == 'backspace') {
+                e.preventDefault();
+                this.clearSearch(e);
+                return false;
+            } else if (this.flags.allSelected && VS.app.hotkeys.printable(e)) {
+                this.clearSearch(e);
+            }
+        },
+
+        // # Focusing Facets #
+
+        // Move focus between facets and inputs. Takes a direction as well as many options
+        // for skipping over inputs and only to facets, placement of cursor position in facet
+        // (i.e. at the end), and selecting the text in the input/facet.
+        focusNextFacet : function(currentView, direction, options) {
+            options = options || {};
+            var viewCount    = this.facetViews.length;
+            var viewPosition = options.viewPosition || this.viewPosition(currentView);
+
+            if (!options.skipToFacet) {
+                // Correct for bouncing between matching text and facet arrays.
+                if (currentView.type == 'text'  && direction > 0) direction -= 1;
+                if (currentView.type == 'facet' && direction < 0) direction += 1;
+            } else if (options.skipToFacet && currentView.type == 'text' &&
+                viewCount == viewPosition && direction >= 0) {
+                // Special case of looping around to a facet from the last search input box.
+                return false;
+            }
+            var view, next = Math.min(viewCount, viewPosition + direction);
+
+            if (currentView.type == 'text') {
+                if (next >= 0 && next < viewCount) {
+                    view = this.facetViews[next];
+                } else if (next == viewCount) {
+                    view = this.inputViews[this.inputViews.length-1];
+                }
+                if (view && options.selectFacet && view.type == 'facet') {
+                    view.selectFacet();
+                } else if (view) {
+                    view.enableEdit();
+                    view.setCursorAtEnd(direction || options.startAtEnd);
+                }
+            } else if (currentView.type == 'facet') {
+                if (options.skipToFacet) {
+                    if (next >= viewCount || next < 0) {
+                        view = _.last(this.inputViews);
+                        view.enableEdit();
+                    } else {
+                        view = this.facetViews[next];
+                        view.enableEdit();
+                        view.setCursorAtEnd(direction || options.startAtEnd);
+                    }
+                } else {
+                    view = this.inputViews[next];
+                    view.enableEdit();
+                }
+            }
+            if (options.selectText) view.selectText();
+            this.resizeFacets();
+
+            return true;
+        },
+
+        maybeFocusSearch : function(e) {
+            if (this.app.options.readOnly) return;
+            if ($(e.target).is('.VS-search-box') ||
+                $(e.target).is('.VS-search-inner') ||
+                e.type == 'keydown') {
+                this.focusSearch(e);
+            }
+        },
+
+        // Bring focus to last input field.
+        focusSearch : function(e, selectText) {
+            if (this.app.options.readOnly) return;
+            var view = this.inputViews[this.inputViews.length-1];
+            view.enableEdit(selectText);
+            if (!selectText) view.setCursorAtEnd(-1);
+            if (e.type == 'keydown') {
+                view.keydown(e);
+                view.box.trigger('keydown');
+            }
+            _.defer(_.bind(function() {
+                if (!this.$('input:focus').length) {
+                    view.enableEdit(selectText);
+                }
+            }, this));
+        },
+
+        // Double-clicking on the search wrapper should select the existing text in
+        // the last search input. Also start the triple-click timer.
+        highlightSearch : function(e) {
+            if (this.app.options.readOnly) return;
+            if ($(e.target).is('.VS-search-box') ||
+                $(e.target).is('.VS-search-inner') ||
+                e.type == 'keydown') {
+                var lastinput = this.inputViews[this.inputViews.length-1];
+                lastinput.startTripleClickTimer();
+                this.focusSearch(e, true);
+            }
+        },
+
+        maybeTripleClick : function(e) {
+            var lastinput = this.inputViews[this.inputViews.length-1];
+            return lastinput.maybeTripleClick(e);
+        },
+
+        // Used to show the user is focused on some input inside the search box.
+        addFocus : function() {
+            if (this.app.options.readOnly) return;
+            this.app.options.callbacks.focus();
+            this.$('.VS-search-box').addClass('VS-focus');
+        },
+
+        // User is no longer focused on anything in the search box.
+        removeFocus : function() {
+            this.app.options.callbacks.blur();
+            var focus = _.any(this.facetViews.concat(this.inputViews), function(view) {
+                return view.isFocused();
+            });
+            if (!focus) this.$('.VS-search-box').removeClass('VS-focus');
+        },
+
+        // Show a menu which adds pre-defined facets to the search box. This is unused for now.
+        showFacetCategoryMenu : function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            if (this.facetCategoryMenu && this.facetCategoryMenu.modes.open == 'is') {
+                return this.facetCategoryMenu.close();
+            }
+
+            var items = [
+                {title: 'Account', onClick: _.bind(this.addFacet, this, 'account', '')},
+                {title: 'Project', onClick: _.bind(this.addFacet, this, 'project', '')},
+                {title: 'Filter', onClick: _.bind(this.addFacet, this, 'filter', '')},
+                {title: 'Access', onClick: _.bind(this.addFacet, this, 'access', '')}
+            ];
+
+            var menu = this.facetCategoryMenu || (this.facetCategoryMenu = new dc.ui.Menu({
+                    items       : items,
+                    standalone  : true
+                }));
+
+            this.$('.VS-icon-search').after(menu.render().open().content);
+            return false;
+        }
+
+    });
+
+})();
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// This is the visual search facet that holds the category and its autocompleted
+// input field.
+    VS.ui.SearchFacet = Backbone.View.extend({
+
+        type : 'facet',
+
+        className : 'search_facet',
+
+        events : {
+            'click .category'           : 'selectFacet',
+            'keydown input'             : 'keydown',
+            'mousedown input'           : 'enableEdit',
+            'mouseover .VS-icon-cancel' : 'showDelete',
+            'mouseout .VS-icon-cancel'  : 'hideDelete',
+            'click .VS-icon-cancel'     : 'remove'
+        },
+
+        initialize : function(options) {
+            this.options = _.extend({}, this.options, options);
+
+            this.flags = {
+                canClose : false
+            };
+            _.bindAll(this, 'set', 'keydown', 'deselectFacet', 'deferDisableEdit');
+            this.app = this.options.app;
+        },
+
+        // Rendering the facet sets up autocompletion, events on blur, and populates
+        // the facet's input with its starting value.
+        render : function() {
+            $(this.el).html(JST['search_facet']({
+                model : this.model,
+                readOnly: this.app.options.readOnly
+            }));
+
+            this.setMode('not', 'editing');
+            this.setMode('not', 'selected');
+            this.box = this.$('input');
+            this.box.val(this.model.label());
+            this.box.bind('blur', this.deferDisableEdit);
+            // Handle paste events with `propertychange`
+            this.box.bind('input propertychange', this.keydown);
+            this.setupAutocomplete();
+
+            return this;
+        },
+
+        // This method is used to setup the facet's input to auto-grow.
+        // This is defered in the searchBox so it can be attached to the
+        // DOM to get the correct font-size.
+        calculateSize : function() {
+            this.box.autoGrowInput();
+            this.box.unbind('updated.autogrow');
+            this.box.bind('updated.autogrow', _.bind(this.moveAutocomplete, this));
+        },
+
+        // Forces a recalculation of this facet's input field's value. Called when
+        // the facet is focused, removed, or otherwise modified.
+        resize : function(e) {
+            this.box.trigger('resize.autogrow', e);
+        },
+
+        // Watches the facet's input field to see if it matches the beginnings of
+        // words in `autocompleteValues`, which is different for every category.
+        // If the value, when selected from the autocompletion menu, is different
+        // than what it was, commit the facet and search for it.
+        setupAutocomplete : function() {
+            this.box.autocomplete({
+                source    : _.bind(this.autocompleteValues, this),
+                minLength : 0,
+                delay     : 0,
+                autoFocus : true,
+                position  : {offset : "0 5"},
+                create    : _.bind(function(e, ui) {
+                    $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
+                }, this),
+                select    : _.bind(function(e, ui) {
+                    e.preventDefault();
+                    var originalValue = this.model.get('value');
+                    this.set(ui.item.value);
+                    if (originalValue != ui.item.value || this.box.val() != ui.item.value) {
+                        if (this.app.options.autosearch) {
+                            this.search(e);
+                        } else {
+                            this.app.searchBox.renderFacets();
+                            this.app.searchBox.focusNextFacet(this, 1, {viewPosition: this.options.order});
+                        }
+                    }
+                    return false;
+                }, this),
+                open      : _.bind(function(e, ui) {
+                    var box = this.box;
+                    this.box.autocomplete('widget').find('.ui-menu-item').each(function() {
+                        var $value = $(this),
+                            autoCompleteData = $value.data('item.autocomplete') || $value.data('ui-autocomplete-item');
+
+                        if (autoCompleteData['value'] == box.val() && box.data('autocomplete').menu.activate) {
+                            box.data('autocomplete').menu.activate(new $.Event("mouseover"), $value);
+                        }
+                    });
+                }, this)
+            });
+
+            this.box.autocomplete('widget').addClass('VS-interface');
+        },
+
+        // As the facet's input field grows, it may move to the next line in the
+        // search box. `autoGrowInput` triggers an `updated` event on the input
+        // field, which is bound to this method to move the autocomplete menu.
+        moveAutocomplete : function() {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) {
+                autocomplete.menu.element.position({
+                    my        : "left top",
+                    at        : "left bottom",
+                    of        : this.box.data('autocomplete').element,
+                    collision : "flip",
+                    offset    : "0 5"
+                });
+            }
+        },
+
+        // When a user enters a facet and it is being edited, immediately show
+        // the autocomplete menu and size it to match the contents.
+        searchAutocomplete : function(e) {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) {
+                var menu = autocomplete.menu.element;
+                autocomplete.search();
+
+                // Resize the menu based on the correctly measured width of what's bigger:
+                // the menu's original size or the menu items' new size.
+                menu.outerWidth(Math.max(
+                    menu.width('').outerWidth(),
+                    autocomplete.element.outerWidth()
+                ));
+            }
+        },
+
+        // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
+        // and anything else that takes focus out of the facet's input field.
+        closeAutocomplete : function() {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) autocomplete.close();
+        },
+
+        // Search terms used in the autocomplete menu. These are specific to the facet,
+        // and only match for the facet's category. The values are then matched on the
+        // first letter of any word in matches, and finally sorted according to the
+        // value's own category. You can pass `preserveOrder` as an option in the
+        // `facetMatches` callback to skip any further ordering done client-side.
+        autocompleteValues : function(req, resp) {
+            var category = this.model.get('category');
+            var value    = this.model.get('value');
+            var searchTerm = req.term;
+
+            this.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) {
+                options = options || {};
+                matches = matches || [];
+
+                if (searchTerm && value != searchTerm) {
+                    if (options.preserveMatches) {
+                        resp(matches);
+                    } else {
+                        var re = VS.utils.inflector.escapeRegExp(searchTerm || '');
+                        var matcher = new RegExp('\\b' + re, 'i');
+                        matches = $.grep(matches, function(item) {
+                            return matcher.test(item) ||
+                                matcher.test(item.value) ||
+                                matcher.test(item.label);
+                        });
+                    }
+                }
+
+                if (options.preserveOrder) {
+                    resp(matches);
+                } else {
+                    resp(_.sortBy(matches, function(match) {
+                        if (match == value || match.value == value) return '';
+                        else return match;
+                    }));
+                }
+            });
+
+        },
+
+        // Sets the facet's model's value.
+        set : function(value) {
+            if (!value) return;
+            this.model.set({'value': value});
+        },
+
+        // Before the searchBox performs a search, we need to close the
+        // autocomplete menu.
+        search : function(e, direction) {
+            if (!direction) direction = 1;
+            this.closeAutocomplete();
+            this.app.searchBox.searchEvent(e);
+            _.defer(_.bind(function() {
+                this.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order});
+            }, this));
+        },
+
+        // Begin editing the facet's input. This is called when the user enters
+        // the input either from another facet or directly clicking on it.
+        //
+        // This method tells all other facets and inputs to disable so it can have
+        // the sole focus. It also prepares the autocompletion menu.
+        enableEdit : function() {
+            if (this.app.options.readOnly) return;
+            if (this.modes.editing != 'is') {
+                this.setMode('is', 'editing');
+                this.deselectFacet();
+                if (this.box.val() == '') {
+                    this.box.val(this.model.get('value'));
+                }
+            }
+
+            this.flags.canClose = false;
+            this.app.searchBox.disableFacets(this);
+            this.app.searchBox.addFocus();
+            _.defer(_.bind(function() {
+                this.app.searchBox.addFocus();
+            }, this));
+            this.resize();
+            this.searchAutocomplete();
+            this.box.focus();
+        },
+
+        // When the user blurs the input, they may either be going to another input
+        // or off the search box entirely. If they go to another input, this facet
+        // will be instantly disabled, and the canClose flag will be turned back off.
+        //
+        // However, if the user clicks elsewhere on the page, this method starts a timer
+        // that checks if any of the other inputs are selected or are being edited. If
+        // not, then it can finally close itself and its autocomplete menu.
+        deferDisableEdit : function() {
+            this.flags.canClose = true;
+            _.delay(_.bind(function() {
+                if (this.flags.canClose && !this.box.is(':focus') &&
+                    this.modes.editing == 'is' && this.modes.selected != 'is') {
+                    this.disableEdit();
+                }
+            }, this), 250);
+        },
+
+        // Called either by other facets receiving focus or by the timer in `deferDisableEdit`,
+        // this method will turn off the facet, remove any text selection, and close
+        // the autocomplete menu.
+        disableEdit : function() {
+            var newFacetQuery = VS.utils.inflector.trim(this.box.val());
+            if (newFacetQuery != this.model.get('value')) {
+                this.set(newFacetQuery);
+            }
+            this.flags.canClose = false;
+            this.box.selectRange(0, 0);
+            this.box.blur();
+            this.setMode('not', 'editing');
+            this.closeAutocomplete();
+            this.app.searchBox.removeFocus();
+        },
+
+        // Selects the facet, which blurs the facet's input and highlights the facet.
+        // If this is the only facet being selected (and not part of a select all event),
+        // we attach a mouse/keyboard watcher to check if the next action by the user
+        // should delete this facet or just deselect it.
+        selectFacet : function(e) {
+            if (e) e.preventDefault();
+            if (this.app.options.readOnly) return;
+            var allSelected = this.app.searchBox.allSelected();
+            if (this.modes.selected == 'is') return;
+
+            if (this.box.is(':focus')) {
+                this.box.setCursorPosition(0);
+                this.box.blur();
+            }
+
+            this.flags.canClose = false;
+            this.closeAutocomplete();
+            this.setMode('is', 'selected');
+            this.setMode('not', 'editing');
+            if (!allSelected || e) {
+                $(document).unbind('keydown.facet', this.keydown);
+                $(document).unbind('click.facet', this.deselectFacet);
+                _.defer(_.bind(function() {
+                    $(document).unbind('keydown.facet').bind('keydown.facet', this.keydown);
+                    $(document).unbind('click.facet').one('click.facet', this.deselectFacet);
+                }, this));
+                this.app.searchBox.disableFacets(this);
+                this.app.searchBox.addFocus();
+            }
+            return false;
+        },
+
+        // Turns off highlighting on the facet. Called in a variety of ways, this
+        // only deselects the facet if it is selected, and then cleans up the
+        // keyboard/mouse watchers that were created when the facet was first
+        // selected.
+        deselectFacet : function(e) {
+            if (e) e.preventDefault();
+            if (this.modes.selected == 'is') {
+                this.setMode('not', 'selected');
+                this.closeAutocomplete();
+                this.app.searchBox.removeFocus();
+            }
+            $(document).unbind('keydown.facet', this.keydown);
+            $(document).unbind('click.facet', this.deselectFacet);
+            return false;
+        },
+
+        // Is the user currently focused in this facet's input field?
+        isFocused : function() {
+            return this.box.is(':focus');
+        },
+
+        // Hovering over the delete button styles the facet so the user knows that
+        // the delete button will kill the entire facet.
+        showDelete : function() {
+            $(this.el).addClass('search_facet_maybe_delete');
+        },
+
+        // On `mouseout`, the user is no longer hovering on the delete button.
+        hideDelete : function() {
+            $(this.el).removeClass('search_facet_maybe_delete');
+        },
+
+        // When switching between facets, depending on the direction the cursor is
+        // coming from, the cursor in this facet's input field should match the original
+        // direction.
+        setCursorAtEnd : function(direction) {
+            if (direction == -1) {
+                this.box.setCursorPosition(this.box.val().length);
+            } else {
+                this.box.setCursorPosition(0);
+            }
+        },
+
+        // Deletes the facet and sends the cursor over to the nearest input field.
+        remove : function(e) {
+            var committed = this.model.get('value');
+            this.deselectFacet();
+            this.disableEdit();
+            this.app.searchQuery.remove(this.model);
+            if (committed && this.app.options.autosearch) {
+                this.search(e, -1);
+            } else {
+                this.app.searchBox.renderFacets();
+                this.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order});
+            }
+        },
+
+        // Selects the text in the facet's input field. When the user tabs between
+        // facets, convention is to highlight the entire field.
+        selectText: function() {
+            this.box.selectRange(0, this.box.val().length);
+        },
+
+        // Handles all keyboard inputs when in the facet's input field. This checks
+        // for movement between facets and inputs, entering a new value that needs
+        // to be autocompleted, as well as the removal of this facet.
+        keydown : function(e) {
+            var key = VS.app.hotkeys.key(e);
+
+            if (key == 'enter' && this.box.val()) {
+                this.disableEdit();
+                this.search(e);
+            } else if (key == 'left') {
+                if (this.modes.selected == 'is') {
+                    this.deselectFacet();
+                    this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+                } else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+                    this.selectFacet();
+                }
+            } else if (key == 'right') {
+                if (this.modes.selected == 'is') {
+                    e.preventDefault();
+                    this.deselectFacet();
+                    this.setCursorAtEnd(0);
+                    this.enableEdit();
+                } else if (this.box.getCursorPosition() == this.box.val().length) {
+                    e.preventDefault();
+                    this.disableEdit();
+                    this.app.searchBox.focusNextFacet(this, 1);
+                }
+            } else if (VS.app.hotkeys.shift && key == 'tab') {
+                e.preventDefault();
+                this.app.searchBox.focusNextFacet(this, -1, {
+                    startAtEnd  : -1,
+                    skipToFacet : true,
+                    selectText  : true
+                });
+            } else if (key == 'tab') {
+                e.preventDefault();
+                this.app.searchBox.focusNextFacet(this, 1, {
+                    skipToFacet : true,
+                    selectText  : true
+                });
+            } else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) {
+                e.preventDefault();
+                this.app.searchBox.selectAllFacets();
+                return false;
+            } else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') {
+                this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+                this.remove(e);
+            } else if (key == 'backspace') {
+                $(document).on('keydown.backspace', function(e) {
+                    if (VS.app.hotkeys.key(e) === 'backspace') {
+                        e.preventDefault();
+                    }
+                });
+
+                $(document).on('keyup.backspace', function(e) {
+                    $(document).off('.backspace');
+                });
+
+                if (this.modes.selected == 'is') {
+                    e.preventDefault();
+                    this.remove(e);
+                } else if (this.box.getCursorPosition() == 0 &&
+                    !this.box.getSelection().length) {
+                    e.preventDefault();
+                    this.selectFacet();
+                }
+                e.stopPropagation();
+            }
+
+            // Handle paste events
+            if (e.which == null) {
+                // this.searchAutocomplete(e);
+                _.defer(_.bind(this.resize, this, e));
+            } else {
+                this.resize(e);
+            }
+        }
+
+    });
+
+})();
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// This is the visual search input that is responsible for creating new facets.
+// There is one input placed in between all facets.
+    VS.ui.SearchInput = Backbone.View.extend({
+
+        type : 'text',
+
+        className : 'search_input ui-menu',
+
+        events : {
+            'keypress input'  : 'keypress',
+            'keydown input'   : 'keydown',
+            'keyup input'     : 'keyup',
+            'click input'     : 'maybeTripleClick',
+            'dblclick input'  : 'startTripleClickTimer'
+        },
+
+        initialize : function(options) {
+            this.options = _.extend({}, this.options, options);
+
+            this.app = this.options.app;
+            this.flags = {
+                canClose : false
+            };
+            _.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit');
+        },
+
+        // Rendering the input sets up autocomplete, events on focusing and blurring
+        // the input, and the auto-grow of the input.
+        render : function() {
+            $(this.el).html(JST['search_input']({
+                readOnly: this.app.options.readOnly
+            }));
+
+            this.setMode('not', 'editing');
+            this.setMode('not', 'selected');
+            this.box = this.$('input');
+            this.box.autoGrowInput();
+            this.box.bind('updated.autogrow', this.moveAutocomplete);
+            this.box.bind('blur',  this.deferDisableEdit);
+            this.box.bind('focus', this.addFocus);
+            this.setupAutocomplete();
+
+            return this;
+        },
+
+        // Watches the input and presents an autocompleted menu, taking the
+        // remainder of the input field and adding a separate facet for it.
+        //
+        // See `addTextFacetRemainder` for explanation on how the remainder works.
+        setupAutocomplete : function() {
+            this.box.autocomplete({
+                minLength : this.options.showFacets ? 0 : 1,
+                delay     : 50,
+                autoFocus : true,
+                position  : {offset : "0 -1"},
+                source    : _.bind(this.autocompleteValues, this),
+                // Prevent changing the input value on focus of an option
+                focus     : function() { return false; },
+                create    : _.bind(function(e, ui) {
+                    $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
+                }, this),
+                select    : _.bind(function(e, ui) {
+                    e.preventDefault();
+                    // stopPropogation does weird things in jquery-ui 1.9
+                    // e.stopPropagation();
+                    var remainder = this.addTextFacetRemainder(ui.item.label || ui.item.value);
+                    var position  = this.options.position + (remainder ? 1 : 0);
+                    this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position);
+                    return false;
+                }, this)
+            });
+
+            // Renders the results grouped by the categories they belong to.
+            this.box.data('autocomplete')._renderMenu = function(ul, items) {
+                var category = '';
+                _.each(items, _.bind(function(item, i) {
+                    if (item.category && item.category != category) {
+                        ul.append('<li class="ui-autocomplete-category">'+item.category+'</li>');
+                        category = item.category;
+                    }
+
+                    if(this._renderItemData) {
+                        this._renderItemData(ul, item);
+                    } else {
+                        this._renderItem(ul, item);
+                    }
+
+                }, this));
+            };
+
+            this.box.autocomplete('widget').addClass('VS-interface');
+        },
+
+        // Search terms used in the autocomplete menu. The values are matched on the
+        // first letter of any word in matches, and finally sorted according to the
+        // value's own category. You can pass `preserveOrder` as an option in the
+        // `facetMatches` callback to skip any further ordering done client-side.
+        autocompleteValues : function(req, resp) {
+            var searchTerm = req.term;
+            var lastWord   = searchTerm.match(/\w+\*?$/); // Autocomplete only last word.
+            var re         = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || '');
+            this.app.options.callbacks.facetMatches(function(prefixes, options) {
+                options = options || {};
+                prefixes = prefixes || [];
+
+                // Only match from the beginning of the word.
+                var matcher    = new RegExp('^' + re, 'i');
+                var matches    = $.grep(prefixes, function(item) {
+                    return item && matcher.test(item.label || item);
+                });
+
+                if (options.preserveOrder) {
+                    resp(matches);
+                } else {
+                    resp(_.sortBy(matches, function(match) {
+                        if (match.label) return match.category + '-' + match.label;
+                        else             return match;
+                    }));
+                }
+            });
+
+        },
+
+        // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
+        // and anything else that takes focus out of the facet's input field.
+        closeAutocomplete : function() {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) autocomplete.close();
+        },
+
+        // As the input field grows, it may move to the next line in the
+        // search box. `autoGrowInput` triggers an `updated` event on the input
+        // field, which is bound to this method to move the autocomplete menu.
+        moveAutocomplete : function() {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) {
+                autocomplete.menu.element.position({
+                    my        : "left top",
+                    at        : "left bottom",
+                    of        : this.box.data('autocomplete').element,
+                    collision : "none",
+                    offset    : '0 -1'
+                });
+            }
+        },
+
+        // When a user enters a facet and it is being edited, immediately show
+        // the autocomplete menu and size it to match the contents.
+        searchAutocomplete : function(e) {
+            var autocomplete = this.box.data('autocomplete');
+            if (autocomplete) {
+                var menu = autocomplete.menu.element;
+                autocomplete.search();
+
+                // Resize the menu based on the correctly measured width of what's bigger:
+                // the menu's original size or the menu items' new size.
+                menu.outerWidth(Math.max(
+                    menu.width('').outerWidth(),
+                    autocomplete.element.outerWidth()
+                ));
+            }
+        },
+
+        // If a user searches for "word word category", the category would be
+        // matched and autocompleted, and when selected, the "word word" would
+        // also be caught as the remainder and then added in its own facet.
+        addTextFacetRemainder : function(facetValue) {
+            var boxValue = this.box.val();
+            var lastWord = boxValue.match(/\b(\w+)$/);
+
+            if (!lastWord) {
+                return '';
+            }
+
+            var matcher = new RegExp(lastWord[0], "i");
+            if (facetValue.search(matcher) == 0) {
+                boxValue = boxValue.replace(/\b(\w+)$/, '');
+            }
+            boxValue = boxValue.replace('^\s+|\s+$', '');
+
+            if (boxValue) {
+                this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position);
+            }
+
+            return boxValue;
+        },
+
+        // Directly called to focus the input. This is different from `addFocus`
+        // because this is not called by a focus event. This instead calls a
+        // focus event causing the input to become focused.
+        enableEdit : function(selectText) {
+            this.addFocus();
+            if (selectText) {
+                this.selectText();
+            }
+            this.box.focus();
+        },
+
+        // Event called on user focus on the input. Tells all other input and facets
+        // to give up focus, and starts revving the autocomplete.
+        addFocus : function() {
+            this.flags.canClose = false;
+            if (!this.app.searchBox.allSelected()) {
+                this.app.searchBox.disableFacets(this);
+            }
+            this.app.searchBox.addFocus();
+            this.setMode('is', 'editing');
+            this.setMode('not', 'selected');
+            if (!this.app.searchBox.allSelected()) {
+                this.searchAutocomplete();
+            }
+        },
+
+        // Directly called to blur the input. This is different from `removeFocus`
+        // because this is not called by a blur event.
+        disableEdit : function() {
+            this.box.blur();
+            this.removeFocus();
+        },
+
+        // Event called when user blur's the input, either through the keyboard tabbing
+        // away or the mouse clicking off. Cleans up
+        removeFocus : function() {
+            this.flags.canClose = false;
+            this.app.searchBox.removeFocus();
+            this.setMode('not', 'editing');
+            this.setMode('not', 'selected');
+            this.closeAutocomplete();
+        },
+
+        // When the user blurs the input, they may either be going to another input
+        // or off the search box entirely. If they go to another input, this facet
+        // will be instantly disabled, and the canClose flag will be turned back off.
+        //
+        // However, if the user clicks elsewhere on the page, this method starts a timer
+        // that checks if any of the other inputs are selected or are being edited. If
+        // not, then it can finally close itself and its autocomplete menu.
+        deferDisableEdit : function() {
+            this.flags.canClose = true;
+            _.delay(_.bind(function() {
+                if (this.flags.canClose &&
+                    !this.box.is(':focus') &&
+                    this.modes.editing == 'is') {
+                    this.disableEdit();
+                }
+            }, this), 250);
+        },
+
+        // Starts a timer that will cause a triple-click, which highlights all facets.
+        startTripleClickTimer : function() {
+            this.tripleClickTimer = setTimeout(_.bind(function() {
+                this.tripleClickTimer = null;
+            }, this), 500);
+        },
+
+        // Event on click that checks if a triple click is in play. The
+        // `tripleClickTimer` is counting down, ready to be engaged and intercept
+        // the click event to force a select all instead.
+        maybeTripleClick : function(e) {
+            if (this.app.options.readOnly) return;
+            if (!!this.tripleClickTimer) {
+                e.preventDefault();
+                this.app.searchBox.selectAllFacets();
+                return false;
+            }
+        },
+
+        // Is the user currently focused in the input field?
+        isFocused : function() {
+            return this.box.is(':focus');
+        },
+
+        // When serializing the facets, the inputs need to also have their values represented,
+        // in case they contain text that is not yet faceted (but will be once the search is
+        // completed).
+        value : function() {
+            return this.box.val();
+        },
+
+        // When switching between facets and inputs, depending on the direction the cursor
+        // is coming from, the cursor in this facet's input field should match the original
+        // direction.
+        setCursorAtEnd : function(direction) {
+            if (direction == -1) {
+                this.box.setCursorPosition(this.box.val().length);
+            } else {
+                this.box.setCursorPosition(0);
+            }
+        },
+
+        // Selects the entire range of text in the input. Useful when tabbing between inputs
+        // and facets.
+        selectText : function() {
+            this.box.selectRange(0, this.box.val().length);
+            if (!this.app.searchBox.allSelected()) {
+                this.box.focus();
+            } else {
+                this.setMode('is', 'selected');
+            }
+        },
+
+        // Before the searchBox performs a search, we need to close the
+        // autocomplete menu.
+        search : function(e, direction) {
+            if (!direction) direction = 0;
+            this.closeAutocomplete();
+            this.app.searchBox.searchEvent(e);
+            _.defer(_.bind(function() {
+                this.app.searchBox.focusNextFacet(this, direction);
+            }, this));
+        },
+
+        // Callback fired on key press in the search box. We search when they hit return.
+        keypress : function(e) {
+            var key = VS.app.hotkeys.key(e);
+
+            if (key == 'enter') {
+                return this.search(e, 100);
+            } else if (VS.app.hotkeys.colon(e)) {
+                this.box.trigger('resize.autogrow', e);
+                var query    = this.box.val();
+                var prefixes = [];
+                this.app.options.callbacks.facetMatches(function(p) {
+                    prefixes = p;
+                });
+                var labels   = _.map(prefixes, function(prefix) {
+                    if (prefix.label) return prefix.label;
+                    else              return prefix;
+                });
+                if (_.contains(labels, query)) {
+                    e.preventDefault();
+                    var remainder = this.addTextFacetRemainder(query);
+                    var position  = this.options.position + (remainder?1:0);
+                    this.app.searchBox.addFacet(query, '', position);
+                    return false;
+                }
+            } else if (key == 'backspace') {
+                if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+                    e.preventDefault();
+                    e.stopPropagation();
+                    e.stopImmediatePropagation();
+                    this.app.searchBox.resizeFacets();
+                    return false;
+                }
+            }
+        },
+
+        // Handles all keyboard inputs when in the input field. This checks
+        // for movement between facets and inputs, entering a new value that needs
+        // to be autocompleted, as well as stepping between facets with backspace.
+        keydown : function(e) {
+            var key = VS.app.hotkeys.key(e);
+
+            if (key == 'left') {
+                if (this.box.getCursorPosition() == 0) {
+                    e.preventDefault();
+                    this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+                }
+            } else if (key == 'right') {
+                if (this.box.getCursorPosition() == this.box.val().length) {
+                    e.preventDefault();
+                    this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true});
+                }
+            } else if (VS.app.hotkeys.shift && key == 'tab') {
+                e.preventDefault();
+                this.app.searchBox.focusNextFacet(this, -1, {selectText: true});
+            } else if (key == 'tab') {
+                var value = this.box.val();
+                if (value.length) {
+                    e.preventDefault();
+                    var remainder = this.addTextFacetRemainder(value);
+                    var position  = this.options.position + (remainder?1:0);
+                    if (value != remainder) {
+                        this.app.searchBox.addFacet(value, '', position);
+                    }
+                } else {
+                    var foundFacet = this.app.searchBox.focusNextFacet(this, 0, {
+                        skipToFacet: true,
+                        selectText: true
+                    });
+                    if (foundFacet) {
+                        e.preventDefault();
+                    }
+                }
+            } else if (VS.app.hotkeys.command &&
+                String.fromCharCode(e.which).toLowerCase() == 'a') {
+                e.preventDefault();
+                this.app.searchBox.selectAllFacets();
+                return false;
+            } else if (key == 'backspace' && !this.app.searchBox.allSelected()) {
+                if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+                    e.preventDefault();
+                    this.app.searchBox.focusNextFacet(this, -1, {backspace: true});
+                    return false;
+                }
+            } else if (key == 'end') {
+                var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1];
+                view.setCursorAtEnd(-1);
+            } else if (key == 'home') {
+                var view = this.app.searchBox.inputViews[0];
+                view.setCursorAtEnd(-1);
+            }
+
+        },
+
+        // We should get the value of an input should be done
+        // on keyup since keydown gets the previous value and not the current one
+        keyup : function(e) {
+            this.box.trigger('resize.autogrow', e);
+        }
+
+    });
+
+})();
+
+(function(){
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+    // Makes the view enter a mode. Modes have both a 'mode' and a 'group',
+    // and are mutually exclusive with any other modes in the same group.
+    // Setting will update the view's modes hash, as well as set an HTML class
+    // of *[mode]_[group]* on the view's element. Convenient way to swap styles
+    // and behavior.
+    Backbone.View.prototype.setMode = function(mode, group) {
+        this.modes || (this.modes = {});
+        if (this.modes[group] === mode) return;
+        $(this.el).setMode(mode, group);
+        this.modes[group] = mode;
+    };
+
+})();
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
+// just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
+// on `keydown`.
+//
+// For the most headache-free way to use this utility, check modifier keys,
+// like shift and command, with `VS.app.hotkeys.shift`, and check every other
+// key with `VS.app.hotkeys.key(e) == 'key_name'`.
+    VS.app.hotkeys = {
+
+        // Keys that will be mapped to the `hotkeys` namespace.
+        KEYS: {
+            '16':  'shift',
+            '17':  'command',
+            '91':  'command',
+            '93':  'command',
+            '224': 'command',
+            '13':  'enter',
+            '37':  'left',
+            '38':  'upArrow',
+            '39':  'right',
+            '40':  'downArrow',
+            '46':  'delete',
+            '8':   'backspace',
+            '35':  'end',
+            '36':  'home',
+            '9':   'tab',
+            '188': 'comma'
+        },
+
+        // Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
+        initialize : function() {
+            _.bindAll(this, 'down', 'up', 'blur');
+            $(document).bind('keydown', this.down);
+            $(document).bind('keyup', this.up);
+            $(window).bind('blur', this.blur);
+        },
+
+        // On `keydown`, turn on all keys that match.
+        down : function(e) {
+            var key = this.KEYS[e.which];
+            if (key) this[key] = true;
+        },
+
+        // On `keyup`, turn off all keys that match.
+        up : function(e) {
+            var key = this.KEYS[e.which];
+            if (key) this[key] = false;
+        },
+
+        // If an input is blurred, all keys need to be turned off, since they are no longer
+        // able to modify the document.
+        blur : function(e) {
+            for (var key in this.KEYS) this[this.KEYS[key]] = false;
+        },
+
+        // Check a key from an event and return the common english name.
+        key : function(e) {
+            return this.KEYS[e.which];
+        },
+
+        // Colon is special, since the value is different between browsers.
+        colon : function(e) {
+            var charCode = e.which;
+            return charCode && String.fromCharCode(charCode) == ":";
+        },
+
+        // Check a key from an event and match it against any known characters.
+        // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
+        //
+        // These were determined by looping through every `keyCode` and `charCode` that
+        // resulted from `keydown` and `keypress` events and counting what was printable.
+        printable : function(e) {
+            var code = e.which;
+            if (e.type == 'keydown') {
+                if (code == 32 ||                      // space
+                    (code >= 48 && code <= 90) ||      // 0-1a-z
+                    (code >= 96 && code <= 111) ||     // 0-9+-/*.
+                    (code >= 186 && code <= 192) ||    // ;=,-./^
+                    (code >= 219 && code <= 222)) {    // (\)'
+                    return true;
+                }
+            } else {
+                // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
+                if ((code >= 32 && code <= 126)  ||
+                    (code >= 160 && code <= 500) ||
+                    (String.fromCharCode(code) == ":")) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    };
+
+})();
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// Naive English transformations on words. Only used for a few transformations
+// in VisualSearch.js.
+    VS.utils.inflector = {
+
+        // Delegate to the ECMA5 String.prototype.trim function, if available.
+        trim : function(s) {
+            return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
+        },
+
+        // Escape strings that are going to be used in a regex. Escapes punctuation
+        // that would be incorrect in a regex.
+        escapeRegExp : function(s) {
+            return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
+        }
+    };
+
+})();
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+    $.fn.extend({
+
+        // Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
+        // and are mutually exclusive with any other modes in the same group.
+        // Setting will update the view's modes hash, as well as set an HTML class
+        // of *[mode]_[group]* on the view's element. Convenient way to swap styles
+        // and behavior.
+        setMode : function(state, group) {
+            group    = group || 'mode';
+            var re   = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
+            var mode = (state === null) ? "" : state + "_" + group;
+            this.each(function() {
+                this.className = (this.className.replace(re, '')+' '+mode)
+                    .replace(/\s\s/g, ' ');
+            });
+            return mode;
+        },
+
+        // When attached to an input element, this will cause the width of the input
+        // to match its contents. This calculates the width of the contents of the input
+        // by measuring a hidden shadow div that should match the styling of the input.
+        autoGrowInput: function() {
+            return this.each(function() {
+                var $input  = $(this);
+                var $tester = $('<div />').css({
+                    opacity     : 0,
+                    top         : -9999,
+                    left        : -9999,
+                    position    : 'absolute',
+                    whiteSpace  : 'nowrap'
+                }).addClass('VS-input-width-tester').addClass('VS-interface');
+
+                // Watch for input value changes on all of these events. `resize`
+                // event is called explicitly when the input has been changed without
+                // a single keypress.
+                var events = 'keydown.autogrow keypress.autogrow ' +
+                    'resize.autogrow change.autogrow';
+                $input.next('.VS-input-width-tester').remove();
+                $input.after($tester);
+                $input.unbind(events).bind(events, function(e, realEvent) {
+                    if (realEvent) e = realEvent;
+                    var value = $input.val();
+
+                    // Watching for the backspace key is tricky because it may not
+                    // actually be deleting the character, but instead the key gets
+                    // redirected to move the cursor from facet to facet.
+                    if (VS.app.hotkeys.key(e) == 'backspace') {
+                        var position = $input.getCursorPosition();
+                        if (position > 0) value = value.slice(0, position-1) +
+                        value.slice(position, value.length);
+                    } else if (VS.app.hotkeys.printable(e) &&
+                        !VS.app.hotkeys.command) {
+                        value += String.fromCharCode(e.which);
+                    }
+                    value = value.replace(/&/g, '&amp;')
+                        .replace(/\s/g,'&nbsp;')
+                        .replace(/</g, '&lt;')
+                        .replace(/>/g, '&gt;');
+
+                    $tester.html(value);
+
+                    $input.width($tester.width() + 3 + parseInt($input.css('min-width')));
+                    $input.trigger('updated.autogrow');
+                });
+
+                // Sets the width of the input on initialization.
+                $input.trigger('resize.autogrow');
+            });
+        },
+
+
+        // Cross-browser method used for calculating where the cursor is in an
+        // input field.
+        getCursorPosition: function() {
+            var position = 0;
+            var input    = this.get(0);
+
+            if (document.selection) { // IE
+                input.focus();
+                var sel    = document.selection.createRange();
+                var selLen = document.selection.createRange().text.length;
+                sel.moveStart('character', -input.value.length);
+                position   = sel.text.length - selLen;
+            } else if (input && $(input).is(':visible') &&
+                input.selectionStart != null) { // Firefox/Safari
+                position = input.selectionStart;
+            }
+
+            return position;
+        },
+
+        // A simple proxy for `selectRange` that sets the cursor position in an
+        // input field.
+        setCursorPosition: function(position) {
+            return this.each(function() {
+                return $(this).selectRange(position, position);
+            });
+        },
+
+        // Cross-browser way to select text in an input field.
+        selectRange: function(start, end) {
+            return this.filter(':visible').each(function() {
+                if (this.setSelectionRange) { // FF/Webkit
+                    this.focus();
+                    this.setSelectionRange(start, end);
+                } else if (this.createTextRange) { // IE
+                    var range = this.createTextRange();
+                    range.collapse(true);
+                    range.moveEnd('character', end);
+                    range.moveStart('character', start);
+                    if (end - start >= 0) range.select();
+                }
+            });
+        },
+
+        // Returns an object that contains the text selection range values for
+        // an input field.
+        getSelection: function() {
+            var input = this[0];
+
+            if (input.selectionStart != null) { // FF/Webkit
+                var start = input.selectionStart;
+                var end   = input.selectionEnd;
+                return {
+                    start   : start,
+                    end     : end,
+                    length  : end-start,
+                    text    : input.value.substr(start, end-start)
+                };
+            } else if (document.selection) { // IE
+                var range = document.selection.createRange();
+                if (range) {
+                    var textRange = input.createTextRange();
+                    var copyRange = textRange.duplicate();
+                    textRange.moveToBookmark(range.getBookmark());
+                    copyRange.setEndPoint('EndToStart', textRange);
+                    var start = copyRange.text.length;
+                    var end   = start + range.text.length;
+                    return {
+                        start   : start,
+                        end     : end,
+                        length  : end-start,
+                        text    : range.text
+                    };
+                }
+            }
+            return {start: 0, end: 0, length: 0};
+        }
+
+    });
+
+// Debugging in Internet Explorer. This allows you to use
+// `console.log(['message', var1, var2, ...])`. Just remove the `false` and
+// add your console.logs. This will automatically stringify objects using
+// `JSON.stringify', so you can read what's going out. Think of this as a
+// *Diet Firebug Lite Zero with Lemon*.
+    if (false) {
+        window.console = {};
+        var _$ied;
+        window.console.log = function(msg) {
+            if (_.isArray(msg)) {
+                var message = msg[0];
+                var vars = _.map(msg.slice(1), function(arg) {
+                    return JSON.stringify(arg);
+                }).join(' - ');
+            }
+            if(!_$ied){
+                _$ied = $('<div><ol></ol></div>').css({
+                    'position': 'fixed',
+                    'bottom': 10,
+                    'left': 10,
+                    'zIndex': 20000,
+                    'width': $('body').width() - 80,
+                    'border': '1px solid #000',
+                    'padding': '10px',
+                    'backgroundColor': '#fff',
+                    'fontFamily': 'arial,helvetica,sans-serif',
+                    'fontSize': '11px'
+                });
+                $('body').append(_$ied);
+            }
+            var $message = $('<li>'+message+' - '+vars+'</li>').css({
+                'borderBottom': '1px solid #999999'
+            });
+            _$ied.find('ol').append($message);
+            _.delay(function() {
+                $message.fadeOut(500);
+            }, 5000);
+        };
+
+    }
+
+})();
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// Used to extract keywords and facets from the free text search.
+    var QUOTES_RE   = "('[^']+'|\"[^\"]+\")";
+    var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
+    var CATEGORY_RE = FREETEXT_RE +                     ':\\s*';
+    VS.app.SearchParser = {
+
+        // Matches `category: "free text"`, with and without quotes.
+        ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),
+
+        // Matches a single category without the text. Used to correctly extract facets.
+        CATEGORY   : new RegExp(CATEGORY_RE),
+
+        // Called to parse a query into a collection of `SearchFacet` models.
+        parse : function(instance, query) {
+            var searchFacets = this._extractAllFacets(instance, query);
+            instance.searchQuery.reset(searchFacets);
+            return searchFacets;
+        },
+
+        // Walks the query and extracts facets, categories, and free text.
+        _extractAllFacets : function(instance, query) {
+            var facets = [];
+            var originalQuery = query;
+            while (query) {
+                var category, value;
+                originalQuery = query;
+                var field = this._extractNextField(query);
+                if (!field) {
+                    category = instance.options.remainder;
+                    value    = this._extractSearchText(query);
+                    query    = VS.utils.inflector.trim(query.replace(value, ''));
+                } else if (field.indexOf(':') != -1) {
+                    category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
+                    value    = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
+                    query    = VS.utils.inflector.trim(query.replace(field, ''));
+                } else if (field.indexOf(':') == -1) {
+                    category = instance.options.remainder;
+                    value    = field;
+                    query    = VS.utils.inflector.trim(query.replace(value, ''));
+                }
+
+                if (category && value) {
+                    var searchFacet = new VS.model.SearchFacet({
+                        category : category,
+                        value    : VS.utils.inflector.trim(value),
+                        app      : instance
+                    });
+                    facets.push(searchFacet);
+                }
+                if (originalQuery == query) break;
+            }
+
+            return facets;
+        },
+
+        // Extracts the first field found, capturing any free text that comes
+        // before the category.
+        _extractNextField : function(query) {
+            var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
+            var textMatch = query.match(textRe);
+            if (textMatch && textMatch.length >= 1) {
+                return textMatch[1];
+            } else {
+                return this._extractFirstField(query);
+            }
+        },
+
+        // If there is no free text before the facet, extract the category and value.
+        _extractFirstField : function(query) {
+            var fields = query.match(this.ALL_FIELDS);
+            return fields && fields.length && fields[0];
+        },
+
+        // If the found match is not a category and facet, extract the trimmed free text.
+        _extractSearchText : function(query) {
+            query = query || '';
+            var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
+            return text;
+        }
+
+    };
+
+})();
+
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// The model that holds individual search facets and their categories.
+// Held in a collection by `VS.app.searchQuery`.
+    VS.model.SearchFacet = Backbone.Model.extend({
+
+        // Extract the category and value and serialize it in preparation for
+        // turning the entire searchBox into a search query that can be sent
+        // to the server for parsing and searching.
+        serialize : function() {
+            var category = this.quoteCategory(this.get('category'));
+            var value    = VS.utils.inflector.trim(this.get('value'));
+            var remainder = this.get("app").options.remainder;
+
+            if (!value) return '';
+
+            if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
+                value = this.quoteValue(value);
+            }
+
+            if (category != remainder) {
+                category = category + ': ';
+            } else {
+                category = "";
+            }
+            return category + value;
+        },
+
+        // Wrap categories that have spaces or any kind of quote with opposite matching
+        // quotes to preserve the complex category during serialization.
+        quoteCategory : function(category) {
+            var hasDoubleQuote = (/"/).test(category);
+            var hasSingleQuote = (/'/).test(category);
+            var hasSpace       = (/\s/).test(category);
+
+            if (hasDoubleQuote && !hasSingleQuote) {
+                return "'" + category + "'";
+            } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
+                return '"' + category + '"';
+            } else {
+                return category;
+            }
+        },
+
+        // Wrap values that have quotes in opposite matching quotes. If a value has
+        // both single and double quotes, just use the double quotes.
+        quoteValue : function(value) {
+            var hasDoubleQuote = (/"/).test(value);
+            var hasSingleQuote = (/'/).test(value);
+
+            if (hasDoubleQuote && !hasSingleQuote) {
+                return "'" + value + "'";
+            } else {
+                return '"' + value + '"';
+            }
+        },
+
+        // If provided, use a custom label instead of the raw value.
+        label : function() {
+            return this.get('label') || this.get('value');
+        }
+
+    });
+
+})();
+(function() {
+
+    var $ = jQuery; // Handle namespaced jQuery
+
+// Collection which holds all of the individual facets (category: value).
+// Used for finding and removing specific facets.
+    VS.model.SearchQuery = Backbone.Collection.extend({
+
+        // Model holds the category and value of the facet.
+        model : VS.model.SearchFacet,
+
+        // Turns all of the facets into a single serialized string.
+        serialize : function() {
+            return this.map(function(facet){ return facet.serialize(); }).join(' ');
+        },
+
+        facets : function() {
+            return this.map(function(facet) {
+                var value = {};
+                value[facet.get('category')] = facet.get('value');
+                return value;
+            });
+        },
+
+        // Find a facet by its category. Multiple facets with the same category
+        // is fine, but only the first is returned.
+        find : function(category) {
+            var facet = this.detect(function(facet) {
+                return facet.get('category').toLowerCase() == category.toLowerCase();
+            });
+            return facet && facet.get('value');
+        },
+
+        // Counts the number of times a specific category is in the search query.
+        count : function(category) {
+            return this.select(function(facet) {
+                return facet.get('category').toLowerCase() == category.toLowerCase();
+            }).length;
+        },
+
+        // Returns an array of extracted values from each facet in a category.
+        values : function(category) {
+            var facets = this.select(function(facet) {
+                return facet.get('category').toLowerCase() == category.toLowerCase();
+            });
+            return _.map(facets, function(facet) { return facet.get('value'); });
+        },
+
+        // Checks all facets for matches of either a category or both category and value.
+        has : function(category, value) {
+            return this.any(function(facet) {
+                var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
+                if (!value) return categoryMatched;
+                return categoryMatched && facet.get('value') == value;
+            });
+        },
+
+        // Used to temporarily hide specific categories and serialize the search query.
+        withoutCategory : function() {
+            var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
+            return this.map(function(facet) {
+                if (!_.include(categories, facet.get('category').toLowerCase())) {
+                    return facet.serialize();
+                };
+            }).join(' ');
+        }
+
+    });
+
+})();
+(function(){
+    window.JST = window.JST || {};
+
+    window.JST['search_box'] = _.template('<div class="VS-search <% if (readOnly) { %>VS-readonly<% } %>">\n  <div class="VS-search-box-wrapper VS-search-box">\n    <div class="VS-icon VS-icon-search"></div>\n    <div class="VS-placeholder"></div>\n    <div class="VS-search-inner"></div>\n    <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n  </div>\n</div>');
+    window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n  <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n  <input type="text" class="search_facet_input ui-menu VS-interface" value="" <% if (readOnly) { %>disabled="disabled"<% } %> />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
+    window.JST['search_input'] = _.template('<input type="text" class="ui-menu" <% if (readOnly) { %>disabled="disabled"<% } %> />');
+})();

+ 341 - 0
ambari-web/vendor/styles/visualsearch.css

@@ -0,0 +1,341 @@
+.VS-search .VS-icon {
+    background-repeat: no-repeat;
+    background-position: center center;
+    vertical-align: middle;
+    width: 16px; height: 16px;
+}
+.VS-search .VS-icon-cancel {
+    width: 11px; height: 11px;
+    background-position: center 0;
+    background-image: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAWCAYAAAAW5GZjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAb9JREFUeNqUUr1qAkEQ3j0khQp6kihaeGgEEa18gTQR0iRY+BaBSMDGwidIEUKqFL6BopgqBAJ5AMFGjUU0d4WHEvwJarvZ77gRIzGYgb1hZr+Z75vZ40IIzqTNZrPj8Xicn0wmmcViEXS73aaqqq+BQODG6/W+A8MBNk3zfDAY3C6Xy0O2ZS6X6zMSiVwHg8FHLjtq7Xb7RQKj7BeTzVCgJ5PJU2U0GhUk7REuMpkMi8fjFggeMeecrVYrFRId0CgTAgDDMFg4HLbA8IjJgHNgGEr0er0fQIphUmZAwdSUADUB4RFDsz3oSMF6CLzZkQqgGebz+Z75dDqNdTqdp13bgDmdTj2VSp0oWHg0Gr2UNH2Z/9o+yMv7K4/HY/C/XhDUfr//jl7QQVT9fp/V63VWqVRYt9tliUSCZbPZg1wux9Lp9PqFeK1Wu9A0DdXz7YM87i0FrVZLs4Fi1wmFQh/NZjOmVKvVgq7rR/QflMtlixGedjwcDlUpMQ9tbzalkAAB2/R297mNW+sT2wUbUnA//V/nYrH4QOBNABUQuFQq3TNMuc82sDVrz41G42yvPeODAwZQ0QzwiJEnzLcAAwBJ6WXlwoBgZAAAAABJRU5ErkJggg==");
+    cursor: pointer;
+}
+.VS-search .VS-icon-cancel:hover {
+    background-position: center -11px;
+}
+.VS-search .VS-icon-search {
+    width: 12px; height: 12px;
+    background-image: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUZJREFUeNpUUM2qgmAQzS8NiUgLzTIXLZQW1QuI9AY9QPSW9gQ9QiriwpJQEBVrVWT2d7p2L9xZzDdzZs7M+YYqy/J8Ptu2vd/v4zgeDAaqqk4mE47jar9GnU6nzWbjOA5FUa/Xq0Jns9l8Pud5vkpp58cwAOzhcBhFkeu6GNztdg3D+Db5vo9nOp2iiWGYTqdDCMFe4LquI0aVpGmKR9M0lmUbjQY8YiBJklTb4YkoilBzOBzq9TogeMQIJEmqmlAlo9EIyXa7tSyrKAp4xEBkWUb5q2k8Hh+PR8/zwjCEgufz+aESstvtoKnVan2GgY31kBkEAfT1ej1FUZDiNIIgrFYr9H1ug3teLpfH43G/3/FBUJGu1+s8z8FZLpc0mmiabrfbf5fEumazuVgsTNO8Xq+3242qRNT+G0CMz7IMzH6//xZgAA60tj6rqzxpAAAAAElFTkSuQmCC");
+}
+
+/*------------------------------ RESET + DEFAULT STYLES ---------------------------------*/
+
+/* 
+Eric Meyer's final reset.css
+Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ 
+*/
+.VS-search div, .VS-search span, .VS-search a, .VS-search img,
+.VS-search ul, .VS-search li, .VS-search form, .VS-search label,
+.VS-interface ul, .VS-interface li, .VS-interface {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-weight: inherit;
+    font-style: inherit;
+    font-size: 100%;
+    font-family: inherit;
+    vertical-align: baseline;
+}
+
+.VS-search :focus {
+    outline: 0;
+}
+.VS-search {
+    line-height: 1;
+    color: black;
+}
+.VS-search ol, .VS-search ul {
+    list-style: none;
+}
+
+/* ===================== */
+/* = General and Reset = */
+/* ===================== */
+
+.VS-search {
+    font-family: Arial, sans-serif;
+    color: #373737;
+    font-size: 12px;
+}
+.VS-search input {
+    display: block;
+    border: none;
+    -moz-box-shadow: none;
+    -webkit-box-shadow: none;
+    box-shadow: none;
+    outline: none;
+    margin: 0; padding: 4px;
+    background: transparent;
+    font-size: 16px;
+    line-height: 20px;
+    width: 100%;
+}
+.VS-interface, .VS-search .dialog, .VS-search input {
+    font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif !important;
+    line-height: 1.1em;
+}
+
+/* ========== */
+/* = Layout = */
+/* ========== */
+
+.VS-search .VS-search-box {
+    cursor: text;
+    position: relative;
+    background: transparent;
+    border: 2px solid #ccc;
+    border-radius: 16px; -webkit-border-radius: 16px; -moz-border-radius: 16px;
+    background-color: #fafafa;
+    -webkit-box-shadow: inset 0px 0px 3px #ccc;
+    -moz-box-shadow: inset 0px 0px 3px #ccc;
+    box-shadow: inset 0px 0px 3px #ccc;
+    min-height: 28px;
+    height: auto;
+}
+.VS-search.VS-readonly .VS-search-box {
+    cursor: default;
+}
+.VS-search .VS-search-box.VS-focus {
+    border-color: #acf;
+    -webkit-box-shadow: inset 0px 0px 3px #acf;
+    -moz-box-shadow: inset 0px 0px 3px #acf;
+    box-shadow: inset 0px 0px 3px #acf;
+}
+.VS-search .VS-placeholder {
+    position: absolute;
+    top: 7px;
+    left: 4px;
+    margin: 0 20px 0 22px;
+    color: #808080;
+    font-size: 14px;
+}
+.VS-search .VS-search-box.VS-focus .VS-placeholder,
+.VS-search .VS-search-box .VS-placeholder.VS-hidden {
+    display: none;
+}
+.VS-search .VS-search-inner {
+    position: relative;
+    margin: 0 20px 0 22px;
+    overflow: hidden;
+}
+.VS-search input {
+    width: 100px;
+}
+.VS-search input,
+.VS-search .VS-input-width-tester {
+    padding: 6px 0;
+    float: left;
+    color: #808080;
+    font: 13px/17px Helvetica, Arial;
+}
+.VS-search.VS-focus input {
+    color: #606060;
+}
+.VS-search .VS-icon-search {
+    position: absolute;
+    left: 9px; top: 8px;
+}
+.VS-search .VS-icon-cancel {
+    position: absolute;
+    right: 9px; top: 8px;
+}
+.VS-search.VS-readonly .VS-icon-cancel {
+    display: none;
+}
+
+/* ================ */
+/* = Search Facet = */
+/* ================ */
+
+.VS-search .search_facet {
+    float: left;
+    margin: 0;
+    padding: 0 0 0 14px;
+    position: relative;
+    border: 1px solid transparent;
+    height: 20px;
+    margin: 3px -3px 3px 0;
+}
+.VS-search.VS-readonly .search_facet {
+    padding-left: 0;
+}
+.VS-search .search_facet.is_selected {
+    margin-left: -3px;
+    -webkit-border-radius: 16px;
+    -moz-border-radius: 16px;
+    border-radius: 16px;
+    background-color: #d2e6fd;
+    background-image: -moz-linear-gradient(top, #d2e6fd, #b0d1f9); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#d2e6fd), to(#b0d1f9)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #d2e6fd, #b0d1f9);
+    border: 1px solid #6eadf5;
+}
+.VS-search .search_facet .category {
+    float: left;
+    text-transform: uppercase;
+    font-weight: bold;
+    font-size: 10px;
+    color: #808080;
+    padding: 8px 0 5px;
+    line-height: 13px;
+    cursor: pointer;
+    padding: 4px 0 0;
+}
+.VS-search.VS-readonly .search_facet .category {
+    cursor: default;
+}
+.VS-search .search_facet.is_selected .category {
+    margin-left: 3px;
+}
+.VS-search .search_facet .search_facet_input_container {
+    float: left;
+}
+.VS-search .search_facet input {
+    margin: 0;
+    padding: 0;
+    color: #000;
+    font-size: 13px;
+    line-height: 16px;
+    padding: 5px 0 5px 4px;
+    height: 16px;
+    width: auto;
+    z-index: 100;
+    position: relative;
+    padding-top: 1px;
+    padding-bottom: 2px;
+    padding-right: 3px;
+
+}
+.VS-search .search_facet.is_editing input,
+.VS-search .search_facet.is_selected input {
+    color: #000;
+}
+.VS-search.VS-readonly .search_facet .search_facet_remove {
+    display: none;
+}
+.VS-search .search_facet .search_facet_remove {
+    position: absolute;
+    left: 0;
+    top: 4px;
+}
+.VS-search .search_facet.is_selected .search_facet_remove {
+    opacity: 0.4;
+    left: 3px;
+    filter: alpha(opacity=40);
+    background-position: center -11px;
+}
+.VS-search .search_facet .search_facet_remove:hover {
+    opacity: 1;
+}
+.VS-search .search_facet.is_editing .category,
+.VS-search .search_facet.is_selected .category {
+    color: #000;
+}
+.VS-search .search_facet.search_facet_maybe_delete .category,
+.VS-search .search_facet.search_facet_maybe_delete input {
+    color: darkred;
+}
+
+/* ================ */
+/* = Search Input = */
+/* ================ */
+
+.VS-search .search_input {
+    height: 28px;
+    float: left;
+    margin-left: -1px;
+}
+.VS-search .search_input input {
+    padding: 6px 3px 6px 2px;
+    line-height: 10px;
+    height: 22px;
+    margin-top: -4px;
+    width: 10px;
+    z-index: 100;
+    min-width: 4px;
+    position: relative;
+}
+.VS-search .search_input.is_editing input {
+    color: #202020;
+}
+
+/* ================ */
+/* = Autocomplete = */
+/* ================ */
+
+.ui-helper-hidden-accessible {
+    display: none;
+}
+
+.VS-interface.ui-autocomplete {
+    position: absolute;
+    border: 1px solid #C0C0C0;
+    border-top: 1px solid #D9D9D9;
+    background-color: #F6F6F6;
+    cursor: pointer;
+    z-index: 10000;
+    padding: 0;
+    margin: 0;
+    width: auto;
+    min-width: 80px;
+    max-width: 220px;
+    max-height: 240px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    font-size: 13px;
+    top: 5px;
+    opacity: 0.97;
+    box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -moz-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5);
+}
+.VS-interface.ui-autocomplete .ui-autocomplete-category {
+    text-transform: capitalize;
+    font-size: 11px;
+    padding: 4px 4px 4px;
+    border-top: 1px solid #A2A2A2;
+    border-bottom: 1px solid #A2A2A2;
+    background-color: #B7B7B7;
+    text-shadow: 0 -1px 0 #999;
+    font-weight: bold;
+    color: white;
+    cursor: default;
+}
+.VS-interface.ui-autocomplete .ui-menu-item {
+    float: none;
+}
+.VS-interface.ui-autocomplete .ui-menu-item a {
+    color: #000;
+    outline: none;
+    display: block;
+    padding: 3px 4px 5px;
+    border-radius: none;
+    line-height: 1;
+    background-color: #F8F8F8;
+    background-image: -moz-linear-gradient(top, #F8F8F8, #F3F3F3); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#F8F8F8), to(#F3F3F3)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #F8F8F8, #F3F3F3);
+    border-top: 1px solid #FAFAFA;
+    border-bottom: 1px solid #f0f0f0;
+}
+.VS-interface.ui-autocomplete .ui-menu-item a:active {
+    outline: none;
+}
+.VS-interface.ui-autocomplete .ui-menu-item .ui-state-hover, .VS-interface.ui-autocomplete .ui-menu-item .ui-state-focus {
+    background-color: #6483F7;
+    background-image: -moz-linear-gradient(top, #648bF5, #2465f3); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#648bF5), to(#2465f3)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #648bF5, #2465f3);
+    border-top: 1px solid #5b83ec;
+    border-bottom: 1px solid #1459e9;
+    border-left: none;
+    border-right: none;
+    color: white;
+    margin: 0;
+}
+.VS-interface.ui-autocomplete .ui-corner-all {
+    border-radius: 0;
+}
+.VS-interface.ui-autocomplete li {
+    list-style: none;
+    width: auto;
+}