/**
 * This widget provides a text field that will search any set of data types
 * and show matching results as the user types. Results can be clicked,
 * triggering a callback.
 */
girder.views.SearchFieldWidget = girder.View.extend({
    events: {
        'input .g-search-field': function () {
            var q = this.$('.g-search-field').val();

            if (!q) {
                this.hideResults();
                return;
            }

            if (this.ajaxLock) {
                this.pending = q;
            } else {
                this._doSearch(q);
            }
        },
        'click .g-search-result>a': function (e) {
            var link = $(e.currentTarget);

            this.trigger('g:resultClicked', {
                type: link.attr('resourcetype'),
                id: link.attr('resourceid'),
                text: link.text()
            });
        },
        'keydown .g-search-field': function (e) {
            var code = e.keyCode || e.which;
            var list, pos;
            if (code === 40) { /* down arrow */
                list = this.$('.g-search-result');
                pos = list.index(list.filter('.g-search-selected')) + 1;
                list.removeClass('g-search-selected');
                if (pos < list.length) {
                    list.eq(pos).addClass('g-search-selected');
                }
            } else if (code === 38) { /* up arrow */
                list = this.$('.g-search-result');
                pos = list.index(list.filter('.g-search-selected')) - 1;
                list.removeClass('g-search-selected');
                if (pos === -2) {
                    pos = list.length - 1;
                }
                if (pos >= 0) {
                    list.eq(pos).addClass('g-search-selected');
                }
            } else if (code === 13) { /* enter */
                var link = this.$('.g-search-result.g-search-selected>a');
                if (link.length) {
                    this.trigger('g:resultClicked', {
                        type: link.attr('resourcetype'),
                        id: link.attr('resourceid'),
                        text: link.text()
                    });
                }
            }
        }
    },

    initialize: function (settings) {
        this.ajaxLock = false;
        this.pending = null;

        this.placeholder = settings.placeholder || 'Search...';
        this.types = settings.types || [];
    },

    render: function () {
        this.$el.html(girder.templates.searchField({
            placeholder: this.placeholder
        }));

        this.$('[title]').tooltip({
            placement: 'auto'
        });

        this.$('.g-search-options-button').popover({
            trigger: 'manual',
            placement: 'bottom',
            html: true,
            content: girder.templates.searchHelp()
        }).blur(function () {
            $(this).popover('hide');
        }).click(function () {
            $(this).popover('toggle');
        });

        return this;
    },

    /**
     * Parent views should call this if they wish to hide the result list.
     */
    hideResults: function () {
        this.$('.dropdown').removeClass('open');
        return this;
    },

    /**
     * Parent views should call this if they wish to clear the search text.
     */
    clearText: function () {
        this.$('.g-search-field').val('');
        return this;
    },

    /**
     * Parent views should call this if they wish to reset the search widget,
     * i.e. clear it and hide any results.
     */
    resetState: function () {
        return this.hideResults().clearText();
    },

    _doSearch: function (q) {
        this.ajaxLock = true;
        this.pending = null;

        girder.restRequest({
            path: 'resource/search',
            data: {
                q: q,
                types: JSON.stringify(this.types)
            }
        }).done(_.bind(function (results) {
            this.ajaxLock = false;

            if (this.pending) {
                this._doSearch(this.pending);
            } else {
                var list = this.$('.g-search-results>ul');
                var resources = [];
                _.each(this.types, function (type) {
                    _.each(results[type] || [], function (result) {
                        var text, icon;
                        if (type === 'user') {
                            text = result.firstName + ' ' + result.lastName +
                                ' (' + result.login + ')';
                            icon = 'user';
                        } else if (type === 'group') {
                            text = result.name;
                            icon = 'users';
                        } else if (type === 'collection') {
                            text = result.name;
                            icon = 'sitemap';
                        } else if (type === 'folder') {
                            text = result.name;
                            icon = 'folder';
                        } else if (type === 'item') {
                            text = result.name;
                            icon = 'doc-text-inv';
                        } else {
                            // TODO plugin callback to render results
                            text = '[unknown type]';
                            icon = 'attention';
                        }
                        resources.push({
                            type: type,
                            id: result._id,
                            text: text,
                            icon: icon
                        });
                    }, this);
                }, this);
                list.html(girder.templates.searchResults({
                    results: resources
                }));

                this.$('.dropdown').addClass('open');
            }
        }, this));
    }
});

@@ -877,32 +877,33 @@
  %7D);%0A        %7D,%0A
+%0A
         'keydown
@@ -2235,24 +2235,412 @@
   %7D%0A    %7D,%0A%0A
+    /**%0A     * @param %5Bplaceholder=%22Search...%22%5D The placeholder text for the input field.%0A     * @param %5BgetInfoCallback%5D For custom resource types, this callback can%0A     *        be passed in to resolve their title and icon. This callback should%0A     *        return an object with %22icon%22 and %22text%22 fields if it can resolve%0A     *        the result, or return falsy otherwise.%0A     */%0A
     initiali
@@ -2790,16 +2790,81 @@
 ch...';%0A
+        this.getInfoCallback = settings.getInfoCallback %7C%7C null;%0A
         
@@ -5698,50 +5698,381 @@
     
-// TODO plugin callback to render results%0A
+if (this.getInfoCallback) %7B%0A                                var res = this.getInfoCallback(type, result);%0A                                if (res) %7B%0A                                    text = res.text;%0A                                    icon = res.icon;%0A                                %7D%0A                            %7D%0A                            if (!text %7C%7C !icon) %7B%0A    
     
@@ -6120,16 +6120,20 @@
 type%5D';%0A
+    
         
@@ -6172,16 +6172,46 @@
 ntion';%0A
+                            %7D%0A
         

Add a callback to SearchFieldWidget to enable searching custom types