You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by rz...@apache.org on 2016/01/09 04:57:14 UTC
[1/4] ambari git commit: AMBARI-14583. Import visualsearch.js library
to ambari-web (rzang)
Repository: ambari
Updated Branches:
refs/heads/trunk 6d06814f7 -> c7476d8a3
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/styles/visualsearch.css
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/styles/visualsearch.css b/ambari-web/vendor/styles/visualsearch.css
new file mode 100644
index 0000000..e76b290
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
[2/4] ambari git commit: AMBARI-14583. Import visualsearch.js library
to ambari-web (rzang)
Posted by rz...@apache.org.
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/scripts/visualsearch.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/visualsearch.js b/ambari-web/vendor/scripts/visualsearch.js
new file mode 100644
index 0000000..044977c
--- /dev/null
+++ b/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, '&')
+ .replace(/\s/g,' ')
+ .replace(/</g, '<')
+ .replace(/>/g, '>');
+
+ $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"<% } %> />');
+})();
\ No newline at end of file
[3/4] ambari git commit: AMBARI-14583. Import visualsearch.js library
to ambari-web (rzang)
Posted by rz...@apache.org.
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/scripts/jquery.ui.autocomplete.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/jquery.ui.autocomplete.js b/ambari-web/vendor/scripts/jquery.ui.autocomplete.js
new file mode 100644
index 0000000..b634cce
--- /dev/null
+++ b/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));
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/scripts/jquery.ui.position.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/jquery.ui.position.js b/ambari-web/vendor/scripts/jquery.ui.position.js
new file mode 100644
index 0000000..03f2606
--- /dev/null
+++ b/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 ));
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/scripts/underscore.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/underscore.js b/ambari-web/vendor/scripts/underscore.js
new file mode 100644
index 0000000..80a3d32
--- /dev/null
+++ b/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: {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ }
+ };
+ 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);
\ No newline at end of file
[4/4] ambari git commit: AMBARI-14583. Import visualsearch.js library
to ambari-web (rzang)
Posted by rz...@apache.org.
AMBARI-14583. Import visualsearch.js library to ambari-web (rzang)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/c7476d8a
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/c7476d8a
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/c7476d8a
Branch: refs/heads/trunk
Commit: c7476d8a34151f0dc1ecc0087499e309366edc30
Parents: 6d06814
Author: Richard Zang <rz...@apache.org>
Authored: Fri Jan 8 19:50:57 2016 -0800
Committer: Richard Zang <rz...@apache.org>
Committed: Fri Jan 8 19:50:57 2016 -0800
----------------------------------------------------------------------
LICENSE.txt | 124 ++
ambari-web/app/config.js | 3 +-
ambari-web/app/templates/main/host.hbs | 2 +-
.../templates/main/host/combo_search_box.hbs | 22 +
ambari-web/app/views.js | 1 +
.../app/views/main/host/combo_search_box.js | 135 ++
ambari-web/config.coffee | 9 +-
ambari-web/karma.conf.js | 5 +
ambari-web/vendor/scripts/backbone.js | 1581 ++++++++++++++
.../vendor/scripts/jquery.ui.autocomplete.js | 631 ++++++
ambari-web/vendor/scripts/jquery.ui.position.js | 308 +++
ambari-web/vendor/scripts/underscore.js | 1276 ++++++++++++
ambari-web/vendor/scripts/visualsearch.js | 1969 ++++++++++++++++++
ambari-web/vendor/styles/visualsearch.css | 341 +++
14 files changed, 6404 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/LICENSE.txt
----------------------------------------------------------------------
diff --git a/LICENSE.txt b/LICENSE.txt
index e0ad2d1..f92b489 100644
--- a/LICENSE.txt
+++ b/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):
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/app/config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js
index 098a0dc..f2b7938 100644
--- a/ambari-web/app/config.js
+++ b/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) {
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/app/templates/main/host.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host.hbs b/ambari-web/app/templates/main/host.hbs
index b0f8265..1c575a9 100644
--- a/ambari-web/app/templates/main/host.hbs
+++ b/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"}}
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/app/templates/main/host/combo_search_box.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host/combo_search_box.hbs b/ambari-web/app/templates/main/host/combo_search_box.hbs
new file mode 100644
index 0000000..ea6e2b4
--- /dev/null
+++ b/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"> </div>
+{{/if}}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index c53fdc8..293d223 100644
--- a/ambari-web/app/views.js
+++ b/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');
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/app/views/main/host/combo_search_box.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/combo_search_box.js b/ambari-web/app/views/main/host/combo_search_box.js
new file mode 100644
index 0000000..927a540
--- /dev/null
+++ b/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">»</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;
+ }
+ }
+ }
+ });
+ }
+});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/config.coffee
----------------------------------------------------------------------
diff --git a/ambari-web/config.coffee b/ambari-web/config.coffee
index 2922593..de8566a 100644
--- a/ambari-web/config.coffee
+++ b/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']
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/karma.conf.js
----------------------------------------------------------------------
diff --git a/ambari-web/karma.conf.js b/ambari-web/karma.conf.js
index 3a54978..14af760 100644
--- a/ambari-web/karma.conf.js
+++ b/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',
http://git-wip-us.apache.org/repos/asf/ambari/blob/c7476d8a/ambari-web/vendor/scripts/backbone.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/backbone.js b/ambari-web/vendor/scripts/backbone.js
new file mode 100644
index 0000000..293180d
--- /dev/null
+++ b/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);
\ No newline at end of file