Просмотр исходного кода

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

Richard Zang 9 лет назад
Родитель
Сommit
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.
 
 
+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):
 
 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.
 
 
+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,
 /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):

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

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

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

@@ -54,7 +54,7 @@
       {{/view}}
     </div>
   </div>
-
+  {{view App.MainHostComboSearchBoxView}}
   <table class="table advanced-header-table table-bordered table-striped" id="hosts-table">
     <thead>
       {{#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/views/details');
 require('views/main/host');
+require('views/main/host/combo_search_box');
 require('views/main/host/hosts_table_menu_view');
 require('views/main/host/details');
 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/cubism.v1.js',
           'vendor/scripts/jquery.ui.core.js',
+          'vendor/scripts/jquery.ui.position.js',
           'vendor/scripts/jquery.ui.widget.js',
+          'vendor/scripts/jquery.ui.autocomplete.js',
           'vendor/scripts/jquery.ui.mouse.js',
           'vendor/scripts/jquery.ui.datepicker.js',
           'vendor/scripts/jquery-ui-timepicker-addon.js',
@@ -55,6 +57,9 @@ exports.config =
           'vendor/scripts/jquery.timeago.js',
           'vendor/scripts/jquery.ajax-retry.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-timezone-with-data-2010-2020.js',
           'vendor/scripts/workflow_visualization.js',
@@ -63,6 +68,7 @@ exports.config =
           'vendor/scripts/jquery.flexibleArea.js',
           'vendor/scripts/FileSaver.js',
           'vendor/scripts/Blob.js'
+
           ]
 
     stylesheets:
@@ -79,7 +85,8 @@ exports.config =
           'vendor/styles/rickshaw.css',
           'vendor/styles/bootstrap-combobox.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']
 

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

@@ -57,7 +57,9 @@ module.exports = function(config) {
       'vendor/scripts/d3.v2.js',
       'vendor/scripts/cubism.v1.js',
       'vendor/scripts/jquery.ui.core.js',
+      'vendor/scripts/jquery.ui.position.js',
       'vendor/scripts/jquery.ui.widget.js',
+      'vendor/scripts/jquery.ui.autocomplete.js',
       'vendor/scripts/jquery.ui.mouse.js',
       'vendor/scripts/jquery.ui.datepicker.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.timeago.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/rickshaw.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;
+}