// 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     : this.app.options.delay,
                autoFocus : true,
                position  : {offset : "0 5"},
                create    : _.bind(function(e, ui) {
                    $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
                }, this),
                select    : _.bind(function(e, ui) {
                    e.preventDefault();
                    var originalValue = this.model.get('value');
                    this.set(ui.item.value);
                    if (originalValue != ui.item.value || this.box.val() != ui.item.value) {
                        if (this.app.options.autosearch) {
                            this.search(e);
                        } else {
                            this.app.searchBox.renderFacets();
                            this.app.searchBox.focusNextFacet(this, 1, {viewPosition: this.options.order});
                        }
                    }
                    return false;
                }, this),
                open      : _.bind(function(e, ui) {
                    var box = this.box;
                    this.box.autocomplete('widget').find('.ui-menu-item').each(function() {
                        var $value = $(this),
                            autoCompleteData = $value.data('item.autocomplete') || $value.data('ui-autocomplete-item');

                        if (autoCompleteData['value'] == box.val() && box.data('autocomplete').menu.activate) {
                            box.data('autocomplete').menu.activate(new $.Event("mouseover"), $value);
                        }
                    });
                }, this)
            });

            this.box.autocomplete('widget').addClass('VS-interface');
        },

        // As the facet's input field grows, it may move to the next line in the
        // search box. `autoGrowInput` triggers an `updated` event on the input
        // field, which is bound to this method to move the autocomplete menu.
        moveAutocomplete : function() {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) {
                autocomplete.menu.element.position({
                    my        : "left top",
                    at        : "left bottom",
                    of        : this.box.data('autocomplete').element,
                    collision : "flip",
                    offset    : "0 5"
                });
            }
        },

        // When a user enters a facet and it is being edited, immediately show
        // the autocomplete menu and size it to match the contents.
        searchAutocomplete : function(e) {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) {
                var menu = autocomplete.menu.element;
                autocomplete.search();

                // Resize the menu based on the correctly measured width of what's bigger:
                // the menu's original size or the menu items' new size.
                menu.outerWidth(Math.max(
                    menu.width('').outerWidth(),
                    autocomplete.element.outerWidth()
                ));
            }
        },

        // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
        // and anything else that takes focus out of the facet's input field.
        closeAutocomplete : function() {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) autocomplete.close();
        },

        // Search terms used in the autocomplete menu. These are specific to the facet,
        // and only match for the facet's category. The values are then matched on the
        // first letter of any word in matches, and finally sorted according to the
        // value's own category. You can pass `preserveOrder` as an option in the
        // `facetMatches` callback to skip any further ordering done client-side.
        autocompleteValues : function(req, resp) {
            var category = this.model.get('category');
            var value    = this.model.get('value');
            var searchTerm = req.term;

            this.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) {
                options = options || {};
                matches = matches || [];

                if (searchTerm && value != searchTerm) {
                    if (options.preserveMatches) {
                        resp(matches);
                    } else {
                        var re = VS.utils.inflector.escapeRegExp(searchTerm || '');
                        var matcher = new RegExp('\\b' + re, 'i');
                        matches = $.grep(matches, function(item) {
                            return matcher.test(item) ||
                                matcher.test(item.value) ||
                                matcher.test(item.label);
                        });
                    }
                }

                if (options.preserveOrder) {
                    resp(matches);
                } else {
                    resp(_.sortBy(matches, function(match) {
                        if (match == value || match.value == value) return '';
                        else return match;
                    }));
                }
            });

        },

        // Sets the facet's model's value.
        set : function(value) {
            if (!value) return;
            this.model.set({'value': value});
        },

        // Before the searchBox performs a search, we need to close the
        // autocomplete menu.
        search : function(e, direction) {
            if (!direction) direction = 1;
            this.closeAutocomplete();
            this.app.searchBox.searchEvent(e);
            _.defer(_.bind(function() {
                this.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order});
            }, this));
        },

        // Begin editing the facet's input. This is called when the user enters
        // the input either from another facet or directly clicking on it.
        //
        // This method tells all other facets and inputs to disable so it can have
        // the sole focus. It also prepares the autocompletion menu.
        enableEdit : function() {
            if (this.app.options.readOnly) return;
            if (this.modes.editing != 'is') {
                this.setMode('is', 'editing');
                this.deselectFacet();
                if (this.box.val() == '') {
                    this.box.val(this.model.get('value'));
                }
            }

            this.flags.canClose = false;
            this.app.searchBox.disableFacets(this);
            this.app.searchBox.addFocus();
            _.defer(_.bind(function() {
                this.app.searchBox.addFocus();
            }, this));
            this.resize();
            this.searchAutocomplete();
            this.box.focus();
        },

        // When the user blurs the input, they may either be going to another input
        // or off the search box entirely. If they go to another input, this facet
        // will be instantly disabled, and the canClose flag will be turned back off.
        //
        // However, if the user clicks elsewhere on the page, this method starts a timer
        // that checks if any of the other inputs are selected or are being edited. If
        // not, then it can finally close itself and its autocomplete menu.
        deferDisableEdit : function() {
            this.flags.canClose = true;
            _.delay(_.bind(function() {
                if (this.flags.canClose && !this.box.is(':focus') &&
                    this.modes.editing == 'is' && this.modes.selected != 'is') {
                    this.disableEdit();
                }
            }, this), 250);
        },

        // Called either by other facets receiving focus or by the timer in `deferDisableEdit`,
        // this method will turn off the facet, remove any text selection, and close
        // the autocomplete menu.
        disableEdit : function() {
            var newFacetQuery = VS.utils.inflector.trim(this.box.val());
            if (newFacetQuery != this.model.get('value')) {
                this.set(newFacetQuery);
            }
            this.flags.canClose = false;
            this.box.selectRange(0, 0);
            this.box.blur();
            this.setMode('not', 'editing');
            this.closeAutocomplete();
            this.app.searchBox.removeFocus();
        },

        // Selects the facet, which blurs the facet's input and highlights the facet.
        // If this is the only facet being selected (and not part of a select all event),
        // we attach a mouse/keyboard watcher to check if the next action by the user
        // should delete this facet or just deselect it.
        selectFacet : function(e) {
            if (e) e.preventDefault();
            if (this.app.options.readOnly) return;
            var allSelected = this.app.searchBox.allSelected();
            if (this.modes.selected == 'is') return;

            if (this.box.is(':focus')) {
                this.box.setCursorPosition(0);
                this.box.blur();
            }

            this.flags.canClose = false;
            this.closeAutocomplete();
            this.setMode('is', 'selected');
            this.setMode('not', 'editing');
            if (!allSelected || e) {
                $(document).unbind('keydown.facet', this.keydown);
                $(document).unbind('click.facet', this.deselectFacet);
                _.defer(_.bind(function() {
                    $(document).unbind('keydown.facet').bind('keydown.facet', this.keydown);
                    $(document).unbind('click.facet').one('click.facet', this.deselectFacet);
                }, this));
                this.app.searchBox.disableFacets(this);
                this.app.searchBox.addFocus();
            }
            return false;
        },

        // Turns off highlighting on the facet. Called in a variety of ways, this
        // only deselects the facet if it is selected, and then cleans up the
        // keyboard/mouse watchers that were created when the facet was first
        // selected.
        deselectFacet : function(e) {
            if (e) e.preventDefault();
            if (this.modes.selected == 'is') {
                this.setMode('not', 'selected');
                this.closeAutocomplete();
                this.app.searchBox.removeFocus();
            }
            $(document).unbind('keydown.facet', this.keydown);
            $(document).unbind('click.facet', this.deselectFacet);
            return false;
        },

        // Is the user currently focused in this facet's input field?
        isFocused : function() {
            return this.box.is(':focus');
        },

        // Hovering over the delete button styles the facet so the user knows that
        // the delete button will kill the entire facet.
        showDelete : function() {
            $(this.el).addClass('search_facet_maybe_delete');
        },

        // On `mouseout`, the user is no longer hovering on the delete button.
        hideDelete : function() {
            $(this.el).removeClass('search_facet_maybe_delete');
        },

        // When switching between facets, depending on the direction the cursor is
        // coming from, the cursor in this facet's input field should match the original
        // direction.
        setCursorAtEnd : function(direction) {
            if (direction == -1) {
                this.box.setCursorPosition(this.box.val().length);
            } else {
                this.box.setCursorPosition(0);
            }
        },

        // Deletes the facet and sends the cursor over to the nearest input field.
        remove : function(e) {
            var committed = this.model.get('value');
            this.deselectFacet();
            this.disableEdit();
            this.app.searchQuery.remove(this.model);
            if (committed && this.app.options.autosearch) {
                this.search(e, -1);
            } else {
                this.app.searchBox.renderFacets();
                this.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order});
            }
        },

        // Selects the text in the facet's input field. When the user tabs between
        // facets, convention is to highlight the entire field.
        selectText: function() {
            this.box.selectRange(0, this.box.val().length);
        },

        // Handles all keyboard inputs when in the facet's input field. This checks
        // for movement between facets and inputs, entering a new value that needs
        // to be autocompleted, as well as the removal of this facet.
        keydown : function(e) {
            var key = VS.app.hotkeys.key(e);

            if (key == 'enter' && this.box.val()) {
                this.disableEdit();
                this.search(e);
            } else if (key == 'left') {
                if (this.modes.selected == 'is') {
                    this.deselectFacet();
                    this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
                } else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
                    this.selectFacet();
                }
            } else if (key == 'right') {
                if (this.modes.selected == 'is') {
                    e.preventDefault();
                    this.deselectFacet();
                    this.setCursorAtEnd(0);
                    this.enableEdit();
                } else if (this.box.getCursorPosition() == this.box.val().length) {
                    e.preventDefault();
                    this.disableEdit();
                    this.app.searchBox.focusNextFacet(this, 1);
                }
            } else if (VS.app.hotkeys.shift && key == 'tab') {
                e.preventDefault();
                this.app.searchBox.focusNextFacet(this, -1, {
                    startAtEnd  : -1,
                    skipToFacet : true,
                    selectText  : true
                });
            } else if (key == 'tab') {
                e.preventDefault();
                this.app.searchBox.focusNextFacet(this, 1, {
                    skipToFacet : true,
                    selectText  : true
                });
            } else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) {
                e.preventDefault();
                this.app.searchBox.selectAllFacets();
                return false;
            } else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') {
                this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
                this.remove(e);
            } else if (key == 'backspace') {
                $(document).on('keydown.backspace', function(e) {
                    if (VS.app.hotkeys.key(e) === 'backspace') {
                        e.preventDefault();
                    }
                });

                $(document).on('keyup.backspace', function(e) {
                    $(document).off('.backspace');
                });

                if (this.modes.selected == 'is') {
                    e.preventDefault();
                    this.remove(e);
                } else if (this.box.getCursorPosition() == 0 &&
                    !this.box.getSelection().length) {
                    e.preventDefault();
                    this.selectFacet();
                }
                e.stopPropagation();
            }

            // Handle paste events
            if (e.which == null) {
                // this.searchAutocomplete(e);
                _.defer(_.bind(this.resize, this, e));
            } else {
                this.resize(e);
            }
        }

    });

})();

(function() {

    var $ = jQuery; // Handle namespaced jQuery

// This is the visual search input that is responsible for creating new facets.
// There is one input placed in between all facets.
    VS.ui.SearchInput = Backbone.View.extend({

        type : 'text',

        className : 'search_input ui-menu',

        events : {
            'keypress input'  : 'keypress',
            'keydown input'   : 'keydown',
            'keyup input'     : 'keyup',
            'click input'     : 'maybeTripleClick',
            'dblclick input'  : 'startTripleClickTimer'
        },

        initialize : function(options) {
            this.options = _.extend({}, this.options, options);

            this.app = this.options.app;
            this.flags = {
                canClose : false
            };
            _.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit');
        },

        // Rendering the input sets up autocomplete, events on focusing and blurring
        // the input, and the auto-grow of the input.
        render : function() {
            $(this.el).html(JST['search_input']({
                readOnly: this.app.options.readOnly
            }));

            this.setMode('not', 'editing');
            this.setMode('not', 'selected');
            this.box = this.$('input');
            this.box.autoGrowInput();
            this.box.bind('updated.autogrow', this.moveAutocomplete);
            this.box.bind('blur',  this.deferDisableEdit);
            this.box.bind('focus', this.addFocus);
            this.setupAutocomplete();

            return this;
        },

        // Watches the input and presents an autocompleted menu, taking the
        // remainder of the input field and adding a separate facet for it.
        //
        // See `addTextFacetRemainder` for explanation on how the remainder works.
        setupAutocomplete : function() {
            this.box.autocomplete({
                minLength : this.options.showFacets ? 0 : 1,
                delay     : 50,
                autoFocus : true,
                position  : {offset : "0 -1"},
                source    : _.bind(this.autocompleteValues, this),
                // Prevent changing the input value on focus of an option
                focus     : function() { return false; },
                create    : _.bind(function(e, ui) {
                    $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
                }, this),
                select    : _.bind(function(e, ui) {
                    e.preventDefault();
                    // stopPropogation does weird things in jquery-ui 1.9
                    // e.stopPropagation();
                    var remainder = this.addTextFacetRemainder(ui.item.label || ui.item.value);
                    var position  = this.options.position + (remainder ? 1 : 0);
                    this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position);
                    return false;
                }, this)
            });

            // Renders the results grouped by the categories they belong to.
            this.box.data('autocomplete')._renderMenu = function(ul, items) {
                var category = '';
                _.each(items, _.bind(function(item, i) {
                    if (item.category && item.category != category) {
                        ul.append('<li class="ui-autocomplete-category">'+item.category+'</li>');
                        category = item.category;
                    }

                    if(this._renderItemData) {
                        this._renderItemData(ul, item);
                    } else {
                        this._renderItem(ul, item);
                    }

                }, this));
            };

            this.box.autocomplete('widget').addClass('VS-interface');
        },

        // Search terms used in the autocomplete menu. The values are matched on the
        // first letter of any word in matches, and finally sorted according to the
        // value's own category. You can pass `preserveOrder` as an option in the
        // `facetMatches` callback to skip any further ordering done client-side.
        autocompleteValues : function(req, resp) {
            var searchTerm = req.term;
            var lastWord   = searchTerm.match(/\w+\*?$/); // Autocomplete only last word.
            var re         = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || '');
            this.app.options.callbacks.facetMatches(function(prefixes, options) {
                options = options || {};
                prefixes = prefixes || [];

                // Only match from the beginning of the word.
                var matcher    = new RegExp('^' + re, 'i');
                var matches    = $.grep(prefixes, function(item) {
                    return item && matcher.test(item.label || item);
                });

                if (options.preserveOrder) {
                    resp(matches);
                } else {
                    resp(_.sortBy(matches, function(match) {
                        if (match.label) return match.category + '-' + match.label;
                        else             return match;
                    }));
                }
            });

        },

        // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
        // and anything else that takes focus out of the facet's input field.
        closeAutocomplete : function() {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) autocomplete.close();
        },

        // As the input field grows, it may move to the next line in the
        // search box. `autoGrowInput` triggers an `updated` event on the input
        // field, which is bound to this method to move the autocomplete menu.
        moveAutocomplete : function() {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) {
                autocomplete.menu.element.position({
                    my        : "left top",
                    at        : "left bottom",
                    of        : this.box.data('autocomplete').element,
                    collision : "none",
                    offset    : '0 -1'
                });
            }
        },

        // When a user enters a facet and it is being edited, immediately show
        // the autocomplete menu and size it to match the contents.
        searchAutocomplete : function(e) {
            var autocomplete = this.box.data('autocomplete');
            if (autocomplete) {
                var menu = autocomplete.menu.element;
                autocomplete.search();

                // Resize the menu based on the correctly measured width of what's bigger:
                // the menu's original size or the menu items' new size.
                menu.outerWidth(Math.max(
                    menu.width('').outerWidth(),
                    autocomplete.element.outerWidth()
                ));
            }
        },

        // If a user searches for "word word category", the category would be
        // matched and autocompleted, and when selected, the "word word" would
        // also be caught as the remainder and then added in its own facet.
        addTextFacetRemainder : function(facetValue) {
            var boxValue = this.box.val();
            var lastWord = boxValue.match(/\b(\w+)$/);

            if (!lastWord) {
                return '';
            }

            var matcher = new RegExp(lastWord[0], "i");
            if (facetValue.search(matcher) == 0) {
                boxValue = boxValue.replace(/\b(\w+)$/, '');
            }
            boxValue = boxValue.replace('^\s+|\s+$', '');

            if (boxValue) {
                this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position);
            }

            return boxValue;
        },

        // Directly called to focus the input. This is different from `addFocus`
        // because this is not called by a focus event. This instead calls a
        // focus event causing the input to become focused.
        enableEdit : function(selectText) {
            this.addFocus();
            if (selectText) {
                this.selectText();
            }
            this.box.focus();
        },

        // Event called on user focus on the input. Tells all other input and facets
        // to give up focus, and starts revving the autocomplete.
        addFocus : function() {
            this.flags.canClose = false;
            if (!this.app.searchBox.allSelected()) {
                this.app.searchBox.disableFacets(this);
            }
            this.app.searchBox.addFocus();
            this.setMode('is', 'editing');
            this.setMode('not', 'selected');
            if (!this.app.searchBox.allSelected()) {
                this.searchAutocomplete();
            }
        },

        // Directly called to blur the input. This is different from `removeFocus`
        // because this is not called by a blur event.
        disableEdit : function() {
            this.box.blur();
            this.removeFocus();
        },

        // Event called when user blur's the input, either through the keyboard tabbing
        // away or the mouse clicking off. Cleans up
        removeFocus : function() {
            this.flags.canClose = false;
            this.app.searchBox.removeFocus();
            this.setMode('not', 'editing');
            this.setMode('not', 'selected');
            this.closeAutocomplete();
        },

        // When the user blurs the input, they may either be going to another input
        // or off the search box entirely. If they go to another input, this facet
        // will be instantly disabled, and the canClose flag will be turned back off.
        //
        // However, if the user clicks elsewhere on the page, this method starts a timer
        // that checks if any of the other inputs are selected or are being edited. If
        // not, then it can finally close itself and its autocomplete menu.
        deferDisableEdit : function() {
            this.flags.canClose = true;
            _.delay(_.bind(function() {
                if (this.flags.canClose &&
                    !this.box.is(':focus') &&
                    this.modes.editing == 'is') {
                    this.disableEdit();
                }
            }, this), 250);
        },

        // Starts a timer that will cause a triple-click, which highlights all facets.
        startTripleClickTimer : function() {
            this.tripleClickTimer = setTimeout(_.bind(function() {
                this.tripleClickTimer = null;
            }, this), 500);
        },

        // Event on click that checks if a triple click is in play. The
        // `tripleClickTimer` is counting down, ready to be engaged and intercept
        // the click event to force a select all instead.
        maybeTripleClick : function(e) {
            if (this.app.options.readOnly) return;
            if (!!this.tripleClickTimer) {
                e.preventDefault();
                this.app.searchBox.selectAllFacets();
                return false;
            }
        },

        // Is the user currently focused in the input field?
        isFocused : function() {
            return this.box.is(':focus');
        },

        // When serializing the facets, the inputs need to also have their values represented,
        // in case they contain text that is not yet faceted (but will be once the search is
        // completed).
        value : function() {
            return this.box.val();
        },

        // When switching between facets and inputs, depending on the direction the cursor
        // is coming from, the cursor in this facet's input field should match the original
        // direction.
        setCursorAtEnd : function(direction) {
            if (direction == -1) {
                this.box.setCursorPosition(this.box.val().length);
            } else {
                this.box.setCursorPosition(0);
            }
        },

        // Selects the entire range of text in the input. Useful when tabbing between inputs
        // and facets.
        selectText : function() {
            this.box.selectRange(0, this.box.val().length);
            if (!this.app.searchBox.allSelected()) {
                this.box.focus();
            } else {
                this.setMode('is', 'selected');
            }
        },

        // Before the searchBox performs a search, we need to close the
        // autocomplete menu.
        search : function(e, direction) {
            if (!direction) direction = 0;
            this.closeAutocomplete();
            this.app.searchBox.searchEvent(e);
            _.defer(_.bind(function() {
                this.app.searchBox.focusNextFacet(this, direction);
            }, this));
        },

        // Callback fired on key press in the search box. We search when they hit return.
        keypress : function(e) {
            var key = VS.app.hotkeys.key(e);

            if (key == 'enter') {
                return this.search(e, 100);
            } else if (VS.app.hotkeys.colon(e)) {
                this.box.trigger('resize.autogrow', e);
                var query    = this.box.val();
                var prefixes = [];
                this.app.options.callbacks.facetMatches(function(p) {
                    prefixes = p;
                });
                var labels   = _.map(prefixes, function(prefix) {
                    if (prefix.label) return prefix.label;
                    else              return prefix;
                });
                if (_.contains(labels, query)) {
                    e.preventDefault();
                    var remainder = this.addTextFacetRemainder(query);
                    var position  = this.options.position + (remainder?1:0);
                    this.app.searchBox.addFacet(query, '', position);
                    return false;
                }
            } else if (key == 'backspace') {
                if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                    this.app.searchBox.resizeFacets();
                    return false;
                }
            }
        },

        // Handles all keyboard inputs when in the input field. This checks
        // for movement between facets and inputs, entering a new value that needs
        // to be autocompleted, as well as stepping between facets with backspace.
        keydown : function(e) {
            var key = VS.app.hotkeys.key(e);

            if (key == 'left') {
                if (this.box.getCursorPosition() == 0) {
                    e.preventDefault();
                    this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
                }
            } else if (key == 'right') {
                if (this.box.getCursorPosition() == this.box.val().length) {
                    e.preventDefault();
                    this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true});
                }
            } else if (VS.app.hotkeys.shift && key == 'tab') {
                e.preventDefault();
                this.app.searchBox.focusNextFacet(this, -1, {selectText: true});
            } else if (key == 'tab') {
                var value = this.box.val();
                if (value.length) {
                    e.preventDefault();
                    var remainder = this.addTextFacetRemainder(value);
                    var position  = this.options.position + (remainder?1:0);
                    if (value != remainder) {
                        this.app.searchBox.addFacet(value, '', position);
                    }
                } else {
                    var foundFacet = this.app.searchBox.focusNextFacet(this, 0, {
                        skipToFacet: true,
                        selectText: true
                    });
                    if (foundFacet) {
                        e.preventDefault();
                    }
                }
            } else if (VS.app.hotkeys.command &&
                String.fromCharCode(e.which).toLowerCase() == 'a') {
                e.preventDefault();
                this.app.searchBox.selectAllFacets();
                return false;
            } else if (key == 'backspace' && !this.app.searchBox.allSelected()) {
                if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
                    e.preventDefault();
                    this.app.searchBox.focusNextFacet(this, -1, {backspace: true});
                    return false;
                }
            } else if (key == 'end') {
                var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1];
                view.setCursorAtEnd(-1);
            } else if (key == 'home') {
                var view = this.app.searchBox.inputViews[0];
                view.setCursorAtEnd(-1);
            }

        },

        // We should get the value of an input should be done
        // on keyup since keydown gets the previous value and not the current one
        keyup : function(e) {
            this.box.trigger('resize.autogrow', e);
        }

    });

})();

(function(){

    var $ = jQuery; // Handle namespaced jQuery

    // Makes the view enter a mode. Modes have both a 'mode' and a 'group',
    // and are mutually exclusive with any other modes in the same group.
    // Setting will update the view's modes hash, as well as set an HTML class
    // of *[mode]_[group]* on the view's element. Convenient way to swap styles
    // and behavior.
    Backbone.View.prototype.setMode = function(mode, group) {
        this.modes || (this.modes = {});
        if (this.modes[group] === mode) return;
        $(this.el).setMode(mode, group);
        this.modes[group] = mode;
    };

})();
(function() {

    var $ = jQuery; // Handle namespaced jQuery

// DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
// just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
// on `keydown`.
//
// For the most headache-free way to use this utility, check modifier keys,
// like shift and command, with `VS.app.hotkeys.shift`, and check every other
// key with `VS.app.hotkeys.key(e) == 'key_name'`.
    VS.app.hotkeys = {

        // Keys that will be mapped to the `hotkeys` namespace.
        KEYS: {
            '16':  'shift',
            '17':  'command',
            '91':  'command',
            '93':  'command',
            '224': 'command',
            '13':  'enter',
            '37':  'left',
            '38':  'upArrow',
            '39':  'right',
            '40':  'downArrow',
            '46':  'delete',
            '8':   'backspace',
            '35':  'end',
            '36':  'home',
            '9':   'tab',
            '188': 'comma'
        },

        // Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
        initialize : function() {
            _.bindAll(this, 'down', 'up', 'blur');
            $(document).bind('keydown', this.down);
            $(document).bind('keyup', this.up);
            $(window).bind('blur', this.blur);
        },

        // On `keydown`, turn on all keys that match.
        down : function(e) {
            var key = this.KEYS[e.which];
            if (key) this[key] = true;
        },

        // On `keyup`, turn off all keys that match.
        up : function(e) {
            var key = this.KEYS[e.which];
            if (key) this[key] = false;
        },

        // If an input is blurred, all keys need to be turned off, since they are no longer
        // able to modify the document.
        blur : function(e) {
            for (var key in this.KEYS) this[this.KEYS[key]] = false;
        },

        // Check a key from an event and return the common english name.
        key : function(e) {
            return this.KEYS[e.which];
        },

        // Colon is special, since the value is different between browsers.
        colon : function(e) {
            var charCode = e.which;
            return charCode && String.fromCharCode(charCode) == ":";
        },

        // Check a key from an event and match it against any known characters.
        // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
        //
        // These were determined by looping through every `keyCode` and `charCode` that
        // resulted from `keydown` and `keypress` events and counting what was printable.
        printable : function(e) {
            var code = e.which;
            if (e.type == 'keydown') {
                if (code == 32 ||                      // space
                    (code >= 48 && code <= 90) ||      // 0-1a-z
                    (code >= 96 && code <= 111) ||     // 0-9+-/*.
                    (code >= 186 && code <= 192) ||    // ;=,-./^
                    (code >= 219 && code <= 222)) {    // (\)'
                    return true;
                }
            } else {
                // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
                if ((code >= 32 && code <= 126)  ||
                    (code >= 160 && code <= 500) ||
                    (String.fromCharCode(code) == ":")) {
                    return true;
                }
            }
            return false;
        }

    };

})();
(function() {

    var $ = jQuery; // Handle namespaced jQuery

// Naive English transformations on words. Only used for a few transformations
// in VisualSearch.js.
    VS.utils.inflector = {

        // Delegate to the ECMA5 String.prototype.trim function, if available.
        trim : function(s) {
            return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
        },

        // Escape strings that are going to be used in a regex. Escapes punctuation
        // that would be incorrect in a regex.
        escapeRegExp : function(s) {
            return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
        }
    };

})();
(function() {

    var $ = jQuery; // Handle namespaced jQuery

    $.fn.extend({

        // Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
        // and are mutually exclusive with any other modes in the same group.
        // Setting will update the view's modes hash, as well as set an HTML class
        // of *[mode]_[group]* on the view's element. Convenient way to swap styles
        // and behavior.
        setMode : function(state, group) {
            group    = group || 'mode';
            var re   = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
            var mode = (state === null) ? "" : state + "_" + group;
            this.each(function() {
                this.className = (this.className.replace(re, '')+' '+mode)
                    .replace(/\s\s/g, ' ');
            });
            return mode;
        },

        // When attached to an input element, this will cause the width of the input
        // to match its contents. This calculates the width of the contents of the input
        // by measuring a hidden shadow div that should match the styling of the input.
        autoGrowInput: function() {
            return this.each(function() {
                var $input  = $(this);
                var $tester = $('<div />').css({
                    opacity     : 0,
                    top         : -9999,
                    left        : -9999,
                    position    : 'absolute',
                    whiteSpace  : 'nowrap'
                }).addClass('VS-input-width-tester').addClass('VS-interface');

                // Watch for input value changes on all of these events. `resize`
                // event is called explicitly when the input has been changed without
                // a single keypress.
                var events = 'keydown.autogrow keypress.autogrow ' +
                    'resize.autogrow change.autogrow';
                $input.next('.VS-input-width-tester').remove();
                $input.after($tester);
                $input.unbind(events).bind(events, function(e, realEvent) {
                    if (realEvent) e = realEvent;
                    var value = $input.val();

                    // Watching for the backspace key is tricky because it may not
                    // actually be deleting the character, but instead the key gets
                    // redirected to move the cursor from facet to facet.
                    if (VS.app.hotkeys.key(e) == 'backspace') {
                        var position = $input.getCursorPosition();
                        if (position > 0) value = value.slice(0, position-1) +
                        value.slice(position, value.length);
                    } else if (VS.app.hotkeys.printable(e) &&
                        !VS.app.hotkeys.command) {
                        value += String.fromCharCode(e.which);
                    }
                    value = value.replace(/&/g, '&amp;')
                        .replace(/\s/g,'&nbsp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;');

                    $tester.html(value);

                    $input.width($tester.width() + 3 + parseInt($input.css('min-width')));
                    $input.trigger('updated.autogrow');
                });

                // Sets the width of the input on initialization.
                $input.trigger('resize.autogrow');
            });
        },


        // Cross-browser method used for calculating where the cursor is in an
        // input field.
        getCursorPosition: function() {
            var position = 0;
            var input    = this.get(0);

            if (document.selection) { // IE
                input.focus();
                var sel    = document.selection.createRange();
                var selLen = document.selection.createRange().text.length;
                sel.moveStart('character', -input.value.length);
                position   = sel.text.length - selLen;
            } else if (input && $(input).is(':visible') &&
                input.selectionStart != null) { // Firefox/Safari
                position = input.selectionStart;
            }

            return position;
        },

        // A simple proxy for `selectRange` that sets the cursor position in an
        // input field.
        setCursorPosition: function(position) {
            return this.each(function() {
                return $(this).selectRange(position, position);
            });
        },

        // Cross-browser way to select text in an input field.
        selectRange: function(start, end) {
            return this.filter(':visible').each(function() {
                if (this.setSelectionRange) { // FF/Webkit
                    this.focus();
                    this.setSelectionRange(start, end);
                } else if (this.createTextRange) { // IE
                    var range = this.createTextRange();
                    range.collapse(true);
                    range.moveEnd('character', end);
                    range.moveStart('character', start);
                    if (end - start >= 0) range.select();
                }
            });
        },

        // Returns an object that contains the text selection range values for
        // an input field.
        getSelection: function() {
            var input = this[0];

            if (input.selectionStart != null) { // FF/Webkit
                var start = input.selectionStart;
                var end   = input.selectionEnd;
                return {
                    start   : start,
                    end     : end,
                    length  : end-start,
                    text    : input.value.substr(start, end-start)
                };
            } else if (document.selection) { // IE
                var range = document.selection.createRange();
                if (range) {
                    var textRange = input.createTextRange();
                    var copyRange = textRange.duplicate();
                    textRange.moveToBookmark(range.getBookmark());
                    copyRange.setEndPoint('EndToStart', textRange);
                    var start = copyRange.text.length;
                    var end   = start + range.text.length;
                    return {
                        start   : start,
                        end     : end,
                        length  : end-start,
                        text    : range.text
                    };
                }
            }
            return {start: 0, end: 0, length: 0};
        }

    });

// Debugging in Internet Explorer. This allows you to use
// `console.log(['message', var1, var2, ...])`. Just remove the `false` and
// add your console.logs. This will automatically stringify objects using
// `JSON.stringify', so you can read what's going out. Think of this as a
// *Diet Firebug Lite Zero with Lemon*.
    if (false) {
        window.console = {};
        var _$ied;
        window.console.log = function(msg) {
            if (_.isArray(msg)) {
                var message = msg[0];
                var vars = _.map(msg.slice(1), function(arg) {
                    return JSON.stringify(arg);
                }).join(' - ');
            }
            if(!_$ied){
                _$ied = $('<div><ol></ol></div>').css({
                    'position': 'fixed',
                    'bottom': 10,
                    'left': 10,
                    'zIndex': 20000,
                    'width': $('body').width() - 80,
                    'border': '1px solid #000',
                    'padding': '10px',
                    'backgroundColor': '#fff',
                    'fontFamily': 'arial,helvetica,sans-serif',
                    'fontSize': '11px'
                });
                $('body').append(_$ied);
            }
            var $message = $('<li>'+message+' - '+vars+'</li>').css({
                'borderBottom': '1px solid #999999'
            });
            _$ied.find('ol').append($message);
            _.delay(function() {
                $message.fadeOut(500);
            }, 5000);
        };

    }

})();

(function() {

    var $ = jQuery; // Handle namespaced jQuery

// Used to extract keywords and facets from the free text search.
    var QUOTES_RE   = "('[^']+'|\"[^\"]+\")";
    var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
    var CATEGORY_RE = FREETEXT_RE +                     ':\\s*';
    VS.app.SearchParser = {

        // Matches `category: "free text"`, with and without quotes.
        ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),

        // Matches a single category without the text. Used to correctly extract facets.
        CATEGORY   : new RegExp(CATEGORY_RE),

        // Called to parse a query into a collection of `SearchFacet` models.
        parse : function(instance, query) {
            var searchFacets = this._extractAllFacets(instance, query);
            instance.searchQuery.reset(searchFacets);
            return searchFacets;
        },

        // Walks the query and extracts facets, categories, and free text.
        _extractAllFacets : function(instance, query) {
            var facets = [];
            var originalQuery = query;
            while (query) {
                var category, value;
                originalQuery = query;
                var field = this._extractNextField(query);
                if (!field) {
                    category = instance.options.remainder;
                    value    = this._extractSearchText(query);
                    query    = VS.utils.inflector.trim(query.replace(value, ''));
                } else if (field.indexOf(':') != -1) {
                    category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
                    value    = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
                    query    = VS.utils.inflector.trim(query.replace(field, ''));
                } else if (field.indexOf(':') == -1) {
                    category = instance.options.remainder;
                    value    = field;
                    query    = VS.utils.inflector.trim(query.replace(value, ''));
                }

                if (category && value) {
                    var searchFacet = new VS.model.SearchFacet({
                        category : category,
                        value    : VS.utils.inflector.trim(value),
                        app      : instance
                    });
                    facets.push(searchFacet);
                }
                if (originalQuery == query) break;
            }

            return facets;
        },

        // Extracts the first field found, capturing any free text that comes
        // before the category.
        _extractNextField : function(query) {
            var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
            var textMatch = query.match(textRe);
            if (textMatch && textMatch.length >= 1) {
                return textMatch[1];
            } else {
                return this._extractFirstField(query);
            }
        },

        // If there is no free text before the facet, extract the category and value.
        _extractFirstField : function(query) {
            var fields = query.match(this.ALL_FIELDS);
            return fields && fields.length && fields[0];
        },

        // If the found match is not a category and facet, extract the trimmed free text.
        _extractSearchText : function(query) {
            query = query || '';
            var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
            return text;
        }

    };

})();

(function() {

    var $ = jQuery; // Handle namespaced jQuery

// The model that holds individual search facets and their categories.
// Held in a collection by `VS.app.searchQuery`.
    VS.model.SearchFacet = Backbone.Model.extend({

        // Extract the category and value and serialize it in preparation for
        // turning the entire searchBox into a search query that can be sent
        // to the server for parsing and searching.
        serialize : function() {
            var category = this.quoteCategory(this.get('category'));
            var value    = VS.utils.inflector.trim(this.get('value'));
            var remainder = this.get("app").options.remainder;

            if (!value) return '';

            if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
                value = this.quoteValue(value);
            }

            if (category != remainder) {
                category = category + ': ';
            } else {
                category = "";
            }
            return category + value;
        },

        // Wrap categories that have spaces or any kind of quote with opposite matching
        // quotes to preserve the complex category during serialization.
        quoteCategory : function(category) {
            var hasDoubleQuote = (/"/).test(category);
            var hasSingleQuote = (/'/).test(category);
            var hasSpace       = (/\s/).test(category);

            if (hasDoubleQuote && !hasSingleQuote) {
                return "'" + category + "'";
            } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
                return '"' + category + '"';
            } else {
                return category;
            }
        },

        // Wrap values that have quotes in opposite matching quotes. If a value has
        // both single and double quotes, just use the double quotes.
        quoteValue : function(value) {
            var hasDoubleQuote = (/"/).test(value);
            var hasSingleQuote = (/'/).test(value);

            if (hasDoubleQuote && !hasSingleQuote) {
                return "'" + value + "'";
            } else {
                return '"' + value + '"';
            }
        },

        // If provided, use a custom label instead of the raw value.
        label : function() {
            return this.get('label') || this.get('value');
        }

    });

})();
(function() {

    var $ = jQuery; // Handle namespaced jQuery

// Collection which holds all of the individual facets (category: value).
// Used for finding and removing specific facets.
    VS.model.SearchQuery = Backbone.Collection.extend({

        // Model holds the category and value of the facet.
        model : VS.model.SearchFacet,

        // Turns all of the facets into a single serialized string.
        serialize : function() {
            return this.map(function(facet){ return facet.serialize(); }).join(' ');
        },

        facets : function() {
            return this.map(function(facet) {
                var value = {};
                value[facet.get('category')] = facet.get('value');
                return value;
            });
        },

        // Find a facet by its category. Multiple facets with the same category
        // is fine, but only the first is returned.
        find : function(category) {
            var facet = this.detect(function(facet) {
                return facet.get('category').toLowerCase() == category.toLowerCase();
            });
            return facet && facet.get('value');
        },

        // Counts the number of times a specific category is in the search query.
        count : function(category) {
            return this.select(function(facet) {
                return facet.get('category').toLowerCase() == category.toLowerCase();
            }).length;
        },

        // Returns an array of extracted values from each facet in a category.
        values : function(category) {
            var facets = this.select(function(facet) {
                return facet.get('category').toLowerCase() == category.toLowerCase();
            });
            return _.map(facets, function(facet) { return facet.get('value'); });
        },

        // Checks all facets for matches of either a category or both category and value.
        has : function(category, value) {
            return this.any(function(facet) {
                var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
                if (!value) return categoryMatched;
                return categoryMatched && facet.get('value') == value;
            });
        },

        // Used to temporarily hide specific categories and serialize the search query.
        withoutCategory : function() {
            var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
            return this.map(function(facet) {
                if (!_.include(categories, facet.get('category').toLowerCase())) {
                    return facet.serialize();
                };
            }).join(' ');
        }

    });

})();
(function(){
    window.JST = window.JST || {};

    window.JST['search_box'] = _.template('<div class="VS-search <% if (readOnly) { %>VS-readonly<% } %>">\n  <div class="VS-search-box-wrapper VS-search-box">\n    <div class="VS-icon VS-icon-search"></div>\n    <div class="VS-placeholder"></div>\n    <div class="VS-search-inner"></div>\n    <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n  </div>\n</div>');
    window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n  <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n  <input type="text" class="search_facet_input ui-menu VS-interface" value="" <% if (readOnly) { %>disabled="disabled"<% } %> />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
    window.JST['search_input'] = _.template('<input type="text" class="ui-menu" <% if (readOnly) { %>disabled="disabled"<% } %> />');
})();