<commit_before>// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.

define(["widgets/js/manager",
        "underscore",
        "backbone",
        "jquery",
        "base/js/namespace",
], function(widgetmanager, _, Backbone, $, IPython){

    var WidgetModel = Backbone.Model.extend({
        constructor: function (widget_manager, model_id, comm) {
            // Constructor
            //
            // Creates a WidgetModel instance.
            //
            // Parameters
            // ----------
            // widget_manager : WidgetManager instance
            // model_id : string
            //      An ID unique to this model.
            // comm : Comm instance (optional)
            this.widget_manager = widget_manager;
            this._buffered_state_diff = {};
            this.pending_msgs = 0;
            this.msg_buffer = null;
            this.state_lock = null;
            this.id = model_id;
            this.views = [];

            if (comm !== undefined) {
                // Remember comm associated with the model.
                this.comm = comm;
                comm.model = this;

                // Hook comm messages up to model.
                comm.on_close($.proxy(this._handle_comm_closed, this));
                comm.on_msg($.proxy(this._handle_comm_msg, this));
            }
            return Backbone.Model.apply(this);
        },

        send: function (content, callbacks) {
            // Send a custom msg over the comm.
            if (this.comm !== undefined) {
                var data = {method: 'custom', content: content};
                this.comm.send(data, callbacks);
                this.pending_msgs++;
            }
        },

        _handle_comm_closed: function (msg) {
            // Handle when a widget is closed.
            this.trigger('comm:close');
            delete this.comm.model; // Delete ref so GC will collect widget model.
            delete this.comm;
            delete this.model_id; // Delete id from model so widget manager cleans up.
            _.each(this.views, function(view, i) {
                view.remove();
            });
        },

        _handle_comm_msg: function (msg) {
            // Handle incoming comm msg.
            var method = msg.content.data.method;
            switch (method) {
                case 'update':
                    this.apply_update(msg.content.data.state);
                    break;
                case 'custom':
                    this.trigger('msg:custom', msg.content.data.content);
                    break;
                case 'display':
                    this.widget_manager.display_view(msg, this);
                    break;
            }
        },

        apply_update: function (state) {
            // Handle when a widget is updated via the python side.
            this.state_lock = state;
            try {
                var that = this;
                WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
                    obj[key] = that._unpack_models(state[key]);
                    return obj;
                }, {})]);
            } finally {
               this.state_lock = null;
            }
        },

        _handle_status: function (msg, callbacks) {
            // Handle status msgs.

            // execution_state : ('busy', 'idle', 'starting')
            if (this.comm !== undefined) {
                if (msg.content.execution_state ==='idle') {
                    // Send buffer if this message caused another message to be
                    // throttled.
                    if (this.msg_buffer !== null &&
                        (this.get('msg_throttle') || 3) === this.pending_msgs) {
                        var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
                        this.comm.send(data, callbacks);
                        this.msg_buffer = null;
                    } else {
                        --this.pending_msgs;
                    }
                }
            }
        },

        callbacks: function(view) {
            // Create msg callbacks for a comm msg.
            var callbacks = this.widget_manager.callbacks(view);

            if (callbacks.iopub === undefined) {
                callbacks.iopub = {};
            }

            var that = this;
            callbacks.iopub.status = function (msg) {
                that._handle_status(msg, callbacks);
            };
            return callbacks;
        },

        set: function(key, val, options) {
            // Set a value.
            var return_value = WidgetModel.__super__.set.apply(this, arguments);

            // Backbone only remembers the diff of the most recent set()
            // operation.  Calling set multiple times in a row results in a
            // loss of diff information.  Here we keep our own running diff.
            this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
            return return_value;
        },

        sync: function (method, model, options) {
            // Handle sync to the back-end.  Called when a model.save() is called.

            // Make sure a comm exists.
            var error = options.error || function() {
                console.error('Backbone sync error:', arguments);
            };
            if (this.comm === undefined) {
                error();
                return false;
            }

            // Delete any key value pairs that the back-end already knows about.
            var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
            if (this.state_lock !== null) {
                var keys = Object.keys(this.state_lock);
                for (var i=0; i<keys.length; i++)
                    var key = keys[i];
                    if (attrs[key] === this.state_lock[key]) {
                        delete attrs[key];
                    }
            }

            // Only sync if there are attributes to send to the back-end.
            attrs = this._pack_models(attrs);
            if (_.size(attrs) > 0) {

                // If this message was sent via backbone itself, it will not
                // have any callbacks.  It's important that we create callbacks
                // so we can listen for status messages, etc...
                var callbacks = options.callbacks || this.callbacks();

                // Check throttle.
                if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
                    // The throttle has been exceeded, buffer the current msg so
                    // it can be sent once the kernel has finished processing
                    // some of the existing messages.

                    // Combine updates if it is a 'patch' sync, otherwise replace updates
                    switch (method) {
                        case 'patch':
                            this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
                            break;
                        case 'update':
                        case 'create':
                            this.msg_buffer = attrs;
                            break;
                        default:
                            error();
                            return false;
                    }
                    this.msg_buffer_callbacks = callbacks;

                } else {
                    // We haven't exceeded the throttle, send the message like
                    // normal.
                    var data = {method: 'backbone', sync_data: attrs};
                    this.comm.send(data, callbacks);
                    this.pending_msgs++;
                }
            }
            // Since the comm is a one-way communication, assume the message
            // arrived.  Don't call success since we don't have a model back from the server
            // this means we miss out on the 'sync' event.
            this._buffered_state_diff = {};
        },

        save_changes: function(callbacks) {
            // Push this model's state to the back-end
            //
            // This invokes a Backbone.Sync.
            this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
        },

        _pack_models: function(value) {
            // Replace models with model ids recursively.
            var that = this;
            var packed;
            if (value instanceof Backbone.Model) {
                return "IPY_MODEL_" + value.id;

            } else if ($.isArray(value)) {
                packed = [];
                _.each(value, function(sub_value, key) {
                    packed.push(that._pack_models(sub_value));
                });
                return packed;

            } else if (value instanceof Object) {
                packed = {};
                _.each(value, function(sub_value, key) {
                    packed[key] = that._pack_models(sub_value);
                });
                return packed;

            } else {
                return value;
            }
        },

        _unpack_models: function(value) {
            // Replace model ids with models recursively.
            var that = this;
            var unpacked;
            if ($.isArray(value)) {
                unpacked = [];
                _.each(value, function(sub_value, key) {
                    unpacked.push(that._unpack_models(sub_value));
                });
                return unpacked;

            } else if (value instanceof Object) {
                unpacked = {};
                _.each(value, function(sub_value, key) {
                    unpacked[key] = that._unpack_models(sub_value);
                });
                return unpacked;

            } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
		var model = this.widget_manager.get_model(value.slice(10, value.length));
		if (model) {
		    return model;
		} else {
		    return value;
		}
            } else {
                    return value;
            }
        },

        on_atomic_change: function(keys, callback, context) {
            // on__atomic_change(["key1", "key2"], foo, context) differs from
            // on("change:key1 change:key2", foo, context).
            // If the widget attributes key1 and key2 are both modified,
            // the second form will result in foo being called twice
            // while the first will call foo only once.
            this.on('change', function() {
                if (keys.some(this.hasChanged, this)) {
                    callback.apply(context);
                }
            }, this);

       },
    });
    widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);

    var WidgetView = Backbone.View.extend({
        initialize: function(parameters) {
            // Public constructor.
            this.model.on('change',this.update,this);
            this.options = parameters.options;
            this.child_model_views = {};
            this.child_views = {};
            this.model.views.push(this);
            this.id = this.id || IPython.utils.uuid();
            this.on('displayed', function() {
                this.is_displayed = true;
            }, this);
        },

        update: function(){
            // Triggered on model change.
            //
            // Update view to be consistent with this.model
        },

        create_child_view: function(child_model, options) {
            // Create and return a child view.
            //
            // -given a model and (optionally) a view name if the view name is
            // not given, it defaults to the model's default view attribute.

            // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
            // it would be great to have the widget manager add the cell metadata
            // to the subview without having to add it here.
            options = $.extend({ parent: this }, options || {});
            var child_view = this.model.widget_manager.create_view(child_model, options, this);

            // Associate the view id with the model id.
            if (this.child_model_views[child_model.id] === undefined) {
                this.child_model_views[child_model.id] = [];
            }
            this.child_model_views[child_model.id].push(child_view.id);

            // Remember the view by id.
            this.child_views[child_view.id] = child_view;
            return child_view;
        },

        pop_child_view: function(child_model) {
            // Delete a child view that was previously created using create_child_view.
            var view_ids = this.child_model_views[child_model.id];
            if (view_ids !== undefined) {

                // Only delete the first view in the list.
                var view_id = view_ids[0];
                var view = this.child_views[view_id];
                delete this.child_views[view_id];
                view_ids.splice(0,1);
                child_model.views.pop(view);

                // Remove the view list specific to this model if it is empty.
                if (view_ids.length === 0) {
                    delete this.child_model_views[child_model.id];
                }
                return view;
            }
            return null;
        },

        do_diff: function(old_list, new_list, removed_callback, added_callback) {
            // Difference a changed list and call remove and add callbacks for
            // each removed and added item in the new list.
            //
            // Parameters
            // ----------
            // old_list : array
            // new_list : array
            // removed_callback : Callback(item)
            //      Callback that is called for each item removed.
            // added_callback : Callback(item)
            //      Callback that is called for each item added.

            // Walk the lists until an unequal entry is found.
            var i;
            for (i = 0; i < new_list.length; i++) {
                if (i >= old_list.length || new_list[i] !== old_list[i]) {
                    break;
                }
            }

            // Remove the non-matching items from the old list.
            for (var j = i; j < old_list.length; j++) {
                removed_callback(old_list[j]);
            }

            // Add the rest of the new list items.
            for (; i < new_list.length; i++) {
                added_callback(new_list[i]);
            }
        },

        callbacks: function(){
            // Create msg callbacks for a comm msg.
            return this.model.callbacks(this);
        },

        render: function(){
            // Render the view.
            //
            // By default, this is only called the first time the view is created
        },

        show: function(){
            // Show the widget-area
            if (this.options && this.options.cell &&
                this.options.cell.widget_area !== undefined) {
                this.options.cell.widget_area.show();
            }
        },

        send: function (content) {
            // Send a custom msg associated with this view.
            this.model.send(content, this.callbacks());
        },

        touch: function () {
            this.model.save_changes(this.callbacks());
        },

        after_displayed: function (callback, context) {
            // Calls the callback right away is the view is already displayed
            // otherwise, register the callback to the 'displayed' event.
            if (this.is_displayed) {
                callback.apply(context);
            } else {
                this.on('displayed', callback, context);
            }
        },
    });

    var DOMWidgetView = WidgetView.extend({
        initialize: function (parameters) {
            // Public constructor
            DOMWidgetView.__super__.initialize.apply(this, [parameters]);
            this.on('displayed', this.show, this);
            this.after_displayed(function() {
                this.update_visible(this.model, this.model.get("visible"));
                this.update_css(this.model, this.model.get("_css"));
            }, this);
            this.model.on('msg:custom', this.on_msg, this);
            this.model.on('change:visible', this.update_visible, this);
            this.model.on('change:_css', this.update_css, this);
        },

        on_msg: function(msg) {
            // Handle DOM specific msgs.
            switch(msg.msg_type) {
                case 'add_class':
                    this.add_class(msg.selector, msg.class_list);
                    break;
                case 'remove_class':
                    this.remove_class(msg.selector, msg.class_list);
                    break;
            }
        },

        add_class: function (selector, class_list) {
            // Add a DOM class to an element.
            this._get_selector_element(selector).addClass(class_list);
        },

        remove_class: function (selector, class_list) {
            // Remove a DOM class from an element.
            this._get_selector_element(selector).removeClass(class_list);
        },

        update_visible: function(model, value) {
            // Update visibility
            this.$el.toggle(value);
         },

        update_css: function (model, css) {
            // Update the css styling of this view.
            var e = this.$el;
            if (css === undefined) {return;}
            for (var i = 0; i < css.length; i++) {
                // Apply the css traits to all elements that match the selector.
                var selector = css[i][0];
                var elements = this._get_selector_element(selector);
                if (elements.length > 0) {
                    var trait_key = css[i][1];
                    var trait_value = css[i][2];
                    elements.css(trait_key ,trait_value);
                }
            }
        },

        _get_selector_element: function (selector) {
            // Get the elements via the css selector.
            var elements;
            if (!selector) {
                elements = this.$el;
            } else {
                elements = this.$el.find(selector).addBack(selector);
            }
            return elements;
        },
    });

    var widget = {
        'WidgetModel': WidgetModel,
        'WidgetView': WidgetView,
        'DOMWidgetView': DOMWidgetView,
    };

    // For backwards compatability.
    $.extend(IPython, widget);

    return widget;
});
<commit_msg>state_lock loop<commit_after>// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.

define(["widgets/js/manager",
        "underscore",
        "backbone",
        "jquery",
        "base/js/namespace",
], function(widgetmanager, _, Backbone, $, IPython){

    var WidgetModel = Backbone.Model.extend({
        constructor: function (widget_manager, model_id, comm) {
            // Constructor
            //
            // Creates a WidgetModel instance.
            //
            // Parameters
            // ----------
            // widget_manager : WidgetManager instance
            // model_id : string
            //      An ID unique to this model.
            // comm : Comm instance (optional)
            this.widget_manager = widget_manager;
            this._buffered_state_diff = {};
            this.pending_msgs = 0;
            this.msg_buffer = null;
            this.state_lock = null;
            this.id = model_id;
            this.views = [];

            if (comm !== undefined) {
                // Remember comm associated with the model.
                this.comm = comm;
                comm.model = this;

                // Hook comm messages up to model.
                comm.on_close($.proxy(this._handle_comm_closed, this));
                comm.on_msg($.proxy(this._handle_comm_msg, this));
            }
            return Backbone.Model.apply(this);
        },

        send: function (content, callbacks) {
            // Send a custom msg over the comm.
            if (this.comm !== undefined) {
                var data = {method: 'custom', content: content};
                this.comm.send(data, callbacks);
                this.pending_msgs++;
            }
        },

        _handle_comm_closed: function (msg) {
            // Handle when a widget is closed.
            this.trigger('comm:close');
            delete this.comm.model; // Delete ref so GC will collect widget model.
            delete this.comm;
            delete this.model_id; // Delete id from model so widget manager cleans up.
            _.each(this.views, function(view, i) {
                view.remove();
            });
        },

        _handle_comm_msg: function (msg) {
            // Handle incoming comm msg.
            var method = msg.content.data.method;
            switch (method) {
                case 'update':
                    this.apply_update(msg.content.data.state);
                    break;
                case 'custom':
                    this.trigger('msg:custom', msg.content.data.content);
                    break;
                case 'display':
                    this.widget_manager.display_view(msg, this);
                    break;
            }
        },

        apply_update: function (state) {
            // Handle when a widget is updated via the python side.
            this.state_lock = state;
            try {
                var that = this;
                WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
                    obj[key] = that._unpack_models(state[key]);
                    return obj;
                }, {})]);
            } finally {
               this.state_lock = null;
            }
        },

        _handle_status: function (msg, callbacks) {
            // Handle status msgs.

            // execution_state : ('busy', 'idle', 'starting')
            if (this.comm !== undefined) {
                if (msg.content.execution_state ==='idle') {
                    // Send buffer if this message caused another message to be
                    // throttled.
                    if (this.msg_buffer !== null &&
                        (this.get('msg_throttle') || 3) === this.pending_msgs) {
                        var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
                        this.comm.send(data, callbacks);
                        this.msg_buffer = null;
                    } else {
                        --this.pending_msgs;
                    }
                }
            }
        },

        callbacks: function(view) {
            // Create msg callbacks for a comm msg.
            var callbacks = this.widget_manager.callbacks(view);

            if (callbacks.iopub === undefined) {
                callbacks.iopub = {};
            }

            var that = this;
            callbacks.iopub.status = function (msg) {
                that._handle_status(msg, callbacks);
            };
            return callbacks;
        },

        set: function(key, val, options) {
            // Set a value.
            var return_value = WidgetModel.__super__.set.apply(this, arguments);

            // Backbone only remembers the diff of the most recent set()
            // operation.  Calling set multiple times in a row results in a
            // loss of diff information.  Here we keep our own running diff.
            this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
            return return_value;
        },

        sync: function (method, model, options) {
            // Handle sync to the back-end.  Called when a model.save() is called.

            // Make sure a comm exists.
            var error = options.error || function() {
                console.error('Backbone sync error:', arguments);
            };
            if (this.comm === undefined) {
                error();
                return false;
            }

            // Delete any key value pairs that the back-end already knows about.
            var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
            if (this.state_lock !== null) {
                var keys = Object.keys(this.state_lock);
                for (var i=0; i<keys.length; i++) {
                    var key = keys[i];
                    if (attrs[key] === this.state_lock[key]) {
                        delete attrs[key];
                    }
                }
            }

            // Only sync if there are attributes to send to the back-end.
            attrs = this._pack_models(attrs);
            if (_.size(attrs) > 0) {

                // If this message was sent via backbone itself, it will not
                // have any callbacks.  It's important that we create callbacks
                // so we can listen for status messages, etc...
                var callbacks = options.callbacks || this.callbacks();

                // Check throttle.
                if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
                    // The throttle has been exceeded, buffer the current msg so
                    // it can be sent once the kernel has finished processing
                    // some of the existing messages.

                    // Combine updates if it is a 'patch' sync, otherwise replace updates
                    switch (method) {
                        case 'patch':
                            this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
                            break;
                        case 'update':
                        case 'create':
                            this.msg_buffer = attrs;
                            break;
                        default:
                            error();
                            return false;
                    }
                    this.msg_buffer_callbacks = callbacks;

                } else {
                    // We haven't exceeded the throttle, send the message like
                    // normal.
                    var data = {method: 'backbone', sync_data: attrs};
                    this.comm.send(data, callbacks);
                    this.pending_msgs++;
                }
            }
            // Since the comm is a one-way communication, assume the message
            // arrived.  Don't call success since we don't have a model back from the server
            // this means we miss out on the 'sync' event.
            this._buffered_state_diff = {};
        },

        save_changes: function(callbacks) {
            // Push this model's state to the back-end
            //
            // This invokes a Backbone.Sync.
            this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
        },

        _pack_models: function(value) {
            // Replace models with model ids recursively.
            var that = this;
            var packed;
            if (value instanceof Backbone.Model) {
                return "IPY_MODEL_" + value.id;

            } else if ($.isArray(value)) {
                packed = [];
                _.each(value, function(sub_value, key) {
                    packed.push(that._pack_models(sub_value));
                });
                return packed;

            } else if (value instanceof Object) {
                packed = {};
                _.each(value, function(sub_value, key) {
                    packed[key] = that._pack_models(sub_value);
                });
                return packed;

            } else {
                return value;
            }
        },

        _unpack_models: function(value) {
            // Replace model ids with models recursively.
            var that = this;
            var unpacked;
            if ($.isArray(value)) {
                unpacked = [];
                _.each(value, function(sub_value, key) {
                    unpacked.push(that._unpack_models(sub_value));
                });
                return unpacked;

            } else if (value instanceof Object) {
                unpacked = {};
                _.each(value, function(sub_value, key) {
                    unpacked[key] = that._unpack_models(sub_value);
                });
                return unpacked;

            } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
		var model = this.widget_manager.get_model(value.slice(10, value.length));
		if (model) {
		    return model;
		} else {
		    return value;
		}
            } else {
                    return value;
            }
        },

        on_atomic_change: function(keys, callback, context) {
            // on__atomic_change(["key1", "key2"], foo, context) differs from
            // on("change:key1 change:key2", foo, context).
            // If the widget attributes key1 and key2 are both modified,
            // the second form will result in foo being called twice
            // while the first will call foo only once.
            this.on('change', function() {
                if (keys.some(this.hasChanged, this)) {
                    callback.apply(context);
                }
            }, this);

       },
    });
    widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);

    var WidgetView = Backbone.View.extend({
        initialize: function(parameters) {
            // Public constructor.
            this.model.on('change',this.update,this);
            this.options = parameters.options;
            this.child_model_views = {};
            this.child_views = {};
            this.model.views.push(this);
            this.id = this.id || IPython.utils.uuid();
            this.on('displayed', function() {
                this.is_displayed = true;
            }, this);
        },

        update: function(){
            // Triggered on model change.
            //
            // Update view to be consistent with this.model
        },

        create_child_view: function(child_model, options) {
            // Create and return a child view.
            //
            // -given a model and (optionally) a view name if the view name is
            // not given, it defaults to the model's default view attribute.

            // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
            // it would be great to have the widget manager add the cell metadata
            // to the subview without having to add it here.
            options = $.extend({ parent: this }, options || {});
            var child_view = this.model.widget_manager.create_view(child_model, options, this);

            // Associate the view id with the model id.
            if (this.child_model_views[child_model.id] === undefined) {
                this.child_model_views[child_model.id] = [];
            }
            this.child_model_views[child_model.id].push(child_view.id);

            // Remember the view by id.
            this.child_views[child_view.id] = child_view;
            return child_view;
        },

        pop_child_view: function(child_model) {
            // Delete a child view that was previously created using create_child_view.
            var view_ids = this.child_model_views[child_model.id];
            if (view_ids !== undefined) {

                // Only delete the first view in the list.
                var view_id = view_ids[0];
                var view = this.child_views[view_id];
                delete this.child_views[view_id];
                view_ids.splice(0,1);
                child_model.views.pop(view);

                // Remove the view list specific to this model if it is empty.
                if (view_ids.length === 0) {
                    delete this.child_model_views[child_model.id];
                }
                return view;
            }
            return null;
        },

        do_diff: function(old_list, new_list, removed_callback, added_callback) {
            // Difference a changed list and call remove and add callbacks for
            // each removed and added item in the new list.
            //
            // Parameters
            // ----------
            // old_list : array
            // new_list : array
            // removed_callback : Callback(item)
            //      Callback that is called for each item removed.
            // added_callback : Callback(item)
            //      Callback that is called for each item added.

            // Walk the lists until an unequal entry is found.
            var i;
            for (i = 0; i < new_list.length; i++) {
                if (i >= old_list.length || new_list[i] !== old_list[i]) {
                    break;
                }
            }

            // Remove the non-matching items from the old list.
            for (var j = i; j < old_list.length; j++) {
                removed_callback(old_list[j]);
            }

            // Add the rest of the new list items.
            for (; i < new_list.length; i++) {
                added_callback(new_list[i]);
            }
        },

        callbacks: function(){
            // Create msg callbacks for a comm msg.
            return this.model.callbacks(this);
        },

        render: function(){
            // Render the view.
            //
            // By default, this is only called the first time the view is created
        },

        show: function(){
            // Show the widget-area
            if (this.options && this.options.cell &&
                this.options.cell.widget_area !== undefined) {
                this.options.cell.widget_area.show();
            }
        },

        send: function (content) {
            // Send a custom msg associated with this view.
            this.model.send(content, this.callbacks());
        },

        touch: function () {
            this.model.save_changes(this.callbacks());
        },

        after_displayed: function (callback, context) {
            // Calls the callback right away is the view is already displayed
            // otherwise, register the callback to the 'displayed' event.
            if (this.is_displayed) {
                callback.apply(context);
            } else {
                this.on('displayed', callback, context);
            }
        },
    });

    var DOMWidgetView = WidgetView.extend({
        initialize: function (parameters) {
            // Public constructor
            DOMWidgetView.__super__.initialize.apply(this, [parameters]);
            this.on('displayed', this.show, this);
            this.after_displayed(function() {
                this.update_visible(this.model, this.model.get("visible"));
                this.update_css(this.model, this.model.get("_css"));
            }, this);
            this.model.on('msg:custom', this.on_msg, this);
            this.model.on('change:visible', this.update_visible, this);
            this.model.on('change:_css', this.update_css, this);
        },

        on_msg: function(msg) {
            // Handle DOM specific msgs.
            switch(msg.msg_type) {
                case 'add_class':
                    this.add_class(msg.selector, msg.class_list);
                    break;
                case 'remove_class':
                    this.remove_class(msg.selector, msg.class_list);
                    break;
            }
        },

        add_class: function (selector, class_list) {
            // Add a DOM class to an element.
            this._get_selector_element(selector).addClass(class_list);
        },

        remove_class: function (selector, class_list) {
            // Remove a DOM class from an element.
            this._get_selector_element(selector).removeClass(class_list);
        },

        update_visible: function(model, value) {
            // Update visibility
            this.$el.toggle(value);
         },

        update_css: function (model, css) {
            // Update the css styling of this view.
            var e = this.$el;
            if (css === undefined) {return;}
            for (var i = 0; i < css.length; i++) {
                // Apply the css traits to all elements that match the selector.
                var selector = css[i][0];
                var elements = this._get_selector_element(selector);
                if (elements.length > 0) {
                    var trait_key = css[i][1];
                    var trait_value = css[i][2];
                    elements.css(trait_key ,trait_value);
                }
            }
        },

        _get_selector_element: function (selector) {
            // Get the elements via the css selector.
            var elements;
            if (!selector) {
                elements = this.$el;
            } else {
                elements = this.$el.find(selector).addBack(selector);
            }
            return elements;
        },
    });

    var widget = {
        'WidgetModel': WidgetModel,
        'WidgetView': WidgetView,
        'DOMWidgetView': DOMWidgetView,
    };

    // For backwards compatability.
    $.extend(IPython, widget);

    return widget;
});
<|endoftext|>


<commit_before>import React, {Component} from 'react'
import {observer} from 'mobx-react'
import BloodTypeFilter from './BTFilterPanel'
import RHFilter from './RHFilterPanel'

@observer
class Search extends Component {

    render(){
        const rhFilt = this.props.store.filterByRH
        const btFilt = this.props.store.filterByBT
       const filterType =  rhFilt && btFilt ?
<BloodTypeFilter/><RHFilter/>
 :
            rhFilt ? <RHFilter/> : btFilt ? <BloodTypeFilter/> : null
       /*this.props.store.filterByRH ? <BloodTypeFilter/>
            : this.props.store.filterByBT ? <RHFilter/> : null*/

        return(
             <form className="form-group">
                Search filters:
                 {filterType}

                        Blood type
                        <input
                            className="check"
                            type="checkbox"
                            value="bt"
                            onChange={this.handleSearchFilter}
                            />

                        Rh factor
                        <input
                            className="check"
                            type="checkbox"
                            value="rh"
                            onChange={this.handleSearchFilter}
                            />

               <input
                    type="text"
                    className="form-control"
                    placeholder="search..."
                    />
                <button type="submit" className="btn btn-submit">Search</button>
            </form>
        )
    }
    handleSearchFilter = (e) => {
        const target = e.target.value
        if (target==='bt')
            this.props.store.searchByBT()
        if (target==='rh')
            this.props.store.searchByRH()
        console.log(`meow: ${this.props.store.filterByRH}`)
    }
}

export default Search<commit_msg>added filter panel<commit_after>import React, {Component} from 'react'
import {observer} from 'mobx-react'
import BloodTypeFilter from './BTFilterPanel'
import RHFilter from './RHFilterPanel'

@observer
class Search extends Component {

    render(){
        const rhFilt = this.props.store.filterByRH
        const btFilt = this.props.store.filterByBT
       const filterType = rhFilt && btFilt ?
<BloodTypeFilter/><RHFilter/>

                            : rhFilt ? <RHFilter/>
                            : btFilt ? <BloodTypeFilter/>
                            : null

        return(
             <form className="form-group">
                Search filters:
                 {filterType}

                        Blood type
                        <input
                            className="check"
                            type="checkbox"
                            value="bt"
                            onChange={this.handleSearchFilter}
                            />

                        Rh factor
                        <input
                            className="check"
                            type="checkbox"
                            value="rh"
                            onChange={this.handleSearchFilter}
                            />

               <input
                    type="text"
                    className="form-control"
                    placeholder="search..."
                    />
                <button type="submit" className="btn btn-submit">Search</button>
            </form>
        )
    }
    handleSearchFilter = (e) => {
        const target = e.target.value
        if (target==='bt')
            this.props.store.searchByBT()
        if (target==='rh')
            this.props.store.searchByRH()
        console.log(`meow: ${this.props.store.filterByRH}`)
    }
}

export default Search<|endoftext|>


<commit_before>'use strict';

import core from 'bower:metal/src/core';
import dom from 'bower:metal/src/dom/dom';
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
import SoyComponent from 'bower:metal/src/soy/SoyComponent';
import 'bower:steel-dropdown/src/Dropdown';
import './Select.soy';

/**
 * Responsible for rendering and handling a custom select component, based
 * on `Dropdown`.
 */
class Select extends SoyComponent {
	/**
	 * Finds the index of the given element in the items array.
	 * @param {!Element} element
	 * @return {number}
	 * @protected
	 */
	findItemIndex_(element) {
		var items = this.element.querySelectorAll('li');
		for (var i = 0; i < items.length; i++) {
			if (items.item(i) === element) {
				return i;
			}
		}
	}

	/**
	 * Focuses the option at the given index.
	 * @param {number} index
	 * @protected
	 */
	focusIndex_(index) {
		var option = this.element.querySelector('.select-option:nth-child(' + (index + 1) + ') a');
		if (option) {
			this.focusedIndex_ = index;
			option.focus();
		}
	}

	/**
	 * Gets the `Dropdown` instance used by this `Select`.
	 * @return {!Dropdown}
	 */
	getDropdown() {
		return this.components[this.id + '-dropdown'];
	}

	/**
	 * Handles a `attrsSynced` event for the dropdown.
	 * @param {!Object} data
	 * @protected
	 */
	handleDropdownAttrsSynced_(data) {
		if (this.openedWithKeyboard_) {
			// This is done on `attrsSynced` because the items need to have already
			// been made visible before we try focusing them.
			this.focusIndex_(0);
			this.openedWithKeyboard_ = false;
		} else if (data.changes.expanded) {
			this.focusedIndex_ = null;
		}
	}

	/**
	 * Handles a `click` event on one of the items. Updates `selectedIndex`
	 * accordingly.
	 * @param {!Event} event
	 * @protected
	 */
	handleItemClick_(event) {
		this.selectedIndex = this.findItemIndex_(event.delegateTarget);
		this.getDropdown().close();
		event.preventDefault();
	}

	/**
	 * Handles a `keydown` event on this component. Handles keyboard controls.
	 * @param {!Event} event
	 * @protected
	 */
	handleKeyDown_(event) {
		if (this.getDropdown().expanded) {
			switch (event.keyCode) {
				case 27:
					this.getDropdown().close();
					break;
				case 38:
					this.focusedIndex_ = core.isDefAndNotNull(this.focusedIndex_) ? this.focusedIndex_ : 1;
					this.focusIndex_(this.focusedIndex_ === 0 ? this.items.length - 1 : this.focusedIndex_ - 1);
					break;
				case 40:
					this.focusedIndex_ = core.isDefAndNotNull(this.focusedIndex_) ? this.focusedIndex_ : -1;
					this.focusIndex_(this.focusedIndex_ === this.items.length - 1 ? 0 : this.focusedIndex_ + 1);
					break;
			}
		} else if ((event.keyCode === 13 || event.keyCode === 32) && dom.hasClass(event.target, 'dropdown-select')) {
			this.openedWithKeyboard_ = true;
			this.getDropdown().open();
			event.preventDefault();
			return;
		}
	}
}

/**
 * Attributes definition.
 * @type {!Object}
 * @static
 */
Select.ATTRS = {
	/**
	 * The CSS class used by the select menu button.
	 * @type {string}
	 * @default 'btn btn-default'
	 */
	buttonClass: {
		validator: core.isString,
		value: 'btn btn-default'
	},

	/**
	 * The name of the hidden input field
	 * @type {string}
	 */
	hiddenInputName: {
		validator: core.isString
	},

	/**
	 * A list representing the select dropdown items. Can be either already a list
	 * of objects specifying both name and value for each item, or just a list of
	 * names, in which case the values will be the indexes where the names show up
	 * on the list.
	 * @type {!Array<string>|!Array<!{name: string, value: string}>}
	 * @default []
	 */
	items: {
		validator: val => val instanceof Array,
		valueFn: function() {
			return [];
		}
	},

	/**
	 * The label that should be used for the select menu when no item is
	 * selected. If not set, the first item will be selected automatically.
	 * @type {string}
	 */
	label: {
		validator: core.isString
	},

	/**
	 * The index of the currently selected item, or -1 if none is selected.
	 * @type {number}
	 */
	selectedIndex: {
		validator: core.isNumber,
		valueFn: function() {
			return this.label || !this.items.length ? -1 : 0;
		}
	}
};

/**
 * Default element classes.
 * @type {string}
 * @static
 */
Select.ELEMENT_CLASSES = 'select';

ComponentRegistry.register('Select', Select);

export default Select;
<commit_msg>Do not scroll page when navigating<commit_after>'use strict';

import core from 'bower:metal/src/core';
import dom from 'bower:metal/src/dom/dom';
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
import SoyComponent from 'bower:metal/src/soy/SoyComponent';
import 'bower:steel-dropdown/src/Dropdown';
import './Select.soy';

/**
 * Responsible for rendering and handling a custom select component, based
 * on `Dropdown`.
 */
class Select extends SoyComponent {
	/**
	 * Finds the index of the given element in the items array.
	 * @param {!Element} element
	 * @return {number}
	 * @protected
	 */
	findItemIndex_(element) {
		var items = this.element.querySelectorAll('li');
		for (var i = 0; i < items.length; i++) {
			if (items.item(i) === element) {
				return i;
			}
		}
	}

	/**
	 * Focuses the option at the given index.
	 * @param {number} index
	 * @protected
	 */
	focusIndex_(index) {
		var option = this.element.querySelector('.select-option:nth-child(' + (index + 1) + ') a');
		if (option) {
			this.focusedIndex_ = index;
			option.focus();
		}
	}

	/**
	 * Gets the `Dropdown` instance used by this `Select`.
	 * @return {!Dropdown}
	 */
	getDropdown() {
		return this.components[this.id + '-dropdown'];
	}

	/**
	 * Handles a `attrsSynced` event for the dropdown.
	 * @param {!Object} data
	 * @protected
	 */
	handleDropdownAttrsSynced_(data) {
		if (this.openedWithKeyboard_) {
			// This is done on `attrsSynced` because the items need to have already
			// been made visible before we try focusing them.
			this.focusIndex_(0);
			this.openedWithKeyboard_ = false;
		} else if (data.changes.expanded) {
			this.focusedIndex_ = null;
		}
	}

	/**
	 * Handles a `click` event on one of the items. Updates `selectedIndex`
	 * accordingly.
	 * @param {!Event} event
	 * @protected
	 */
	handleItemClick_(event) {
		this.selectedIndex = this.findItemIndex_(event.delegateTarget);
		this.getDropdown().close();
		event.preventDefault();
	}

	/**
	 * Handles a `keydown` event on this component. Handles keyboard controls.
	 * @param {!Event} event
	 * @protected
	 */
	handleKeyDown_(event) {
		if (this.getDropdown().expanded) {
			switch (event.keyCode) {
				case 27:
					this.getDropdown().close();
					break;
				case 38:
					this.focusedIndex_ = core.isDefAndNotNull(this.focusedIndex_) ? this.focusedIndex_ : 1;
					this.focusIndex_(this.focusedIndex_ === 0 ? this.items.length - 1 : this.focusedIndex_ - 1);
					event.preventDefault();
					break;
				case 40:
					this.focusedIndex_ = core.isDefAndNotNull(this.focusedIndex_) ? this.focusedIndex_ : -1;
					this.focusIndex_(this.focusedIndex_ === this.items.length - 1 ? 0 : this.focusedIndex_ + 1);
					event.preventDefault();
					break;
			}
		} else if ((event.keyCode === 13 || event.keyCode === 32) && dom.hasClass(event.target, 'dropdown-select')) {
			this.openedWithKeyboard_ = true;
			this.getDropdown().open();
			event.preventDefault();
			return;
		}
	}
}

/**
 * Attributes definition.
 * @type {!Object}
 * @static
 */
Select.ATTRS = {
	/**
	 * The CSS class used by the select menu button.
	 * @type {string}
	 * @default 'btn btn-default'
	 */
	buttonClass: {
		validator: core.isString,
		value: 'btn btn-default'
	},

	/**
	 * The name of the hidden input field
	 * @type {string}
	 */
	hiddenInputName: {
		validator: core.isString
	},

	/**
	 * A list representing the select dropdown items. Can be either already a list
	 * of objects specifying both name and value for each item, or just a list of
	 * names, in which case the values will be the indexes where the names show up
	 * on the list.
	 * @type {!Array<string>|!Array<!{name: string, value: string}>}
	 * @default []
	 */
	items: {
		validator: val => val instanceof Array,
		valueFn: function() {
			return [];
		}
	},

	/**
	 * The label that should be used for the select menu when no item is
	 * selected. If not set, the first item will be selected automatically.
	 * @type {string}
	 */
	label: {
		validator: core.isString
	},

	/**
	 * The index of the currently selected item, or -1 if none is selected.
	 * @type {number}
	 */
	selectedIndex: {
		validator: core.isNumber,
		valueFn: function() {
			return this.label || !this.items.length ? -1 : 0;
		}
	}
};

/**
 * Default element classes.
 * @type {string}
 * @static
 */
Select.ELEMENT_CLASSES = 'select';

ComponentRegistry.register('Select', Select);

export default Select;
<|endoftext|>


<commit_before>// @flow

import React from 'react';
import { compose, lifecycle, withState } from 'recompose';
import _ from 'lodash';
import { scaleOrdinal, schemeCategory10 } from 'd3';
import { renderPlot } from '@oncojs/survivalplot';
import Loader from '@ncigdc/uikit/Loaders/Loader';
import toMap from '@ncigdc/utils/toMap';
import { Row, Column } from '@ncigdc/uikit/Flex';
import Button from '@ncigdc/uikit/Button';
import { Tooltip, withTooltip } from '@ncigdc/uikit/Tooltip';
import Hidden from '@ncigdc/components/Hidden';
import withRouter from '@ncigdc/utils/withRouter';
import { wrapSvg } from '@ncigdc/utils/wrapSvg';
import withSize from '@ncigdc/utils/withSize';
import DownloadVisualizationButton from '@ncigdc/components/DownloadVisualizationButton';
import { visualizingButton } from '@ncigdc/theme/mixins';
import VisualizationHeader from '@ncigdc/components/VisualizationHeader';
import { performanceTracker } from '@ncigdc/utils/analytics';

import './survivalPlot.css';

const CLASS_NAME = 'survival-plot';

type TProps = {
  height: number,
  legend: Array<{
    key: string,
    value: any,
  }>,
  rawData: {
    results: Array<{
      donors: Array<Object>,
      meta: {
        id: string | number,
        label: string,
      },
    }>,
    overallStats: {
      pValue: number,
    },
  },
  setXDomain: Function,
  setSurvivalContainer: Function,
  survivalPlotloading: boolean,
  xDomain: Array<number>,
  survivalContainer: Element,
  setTooltip: Function,
  push: Function,
  uniqueClass: string,
};

const TITLE = 'Overall Survival Plot';

const SVG_MARGINS = {
  top: 15,
  right: 20,
  bottom: 40,
  left: 50,
};

const colors = scaleOrdinal(schemeCategory10);

const styles = {
  pValue: {
    fontSize: '1.1rem',
    height: '1.5rem',
    marginTop: '0.5rem',
    textAlign: 'center',
  },
};

const SurvivalPlotWrapper = ({
  height = 0,
  legend,
  rawData,
  setXDomain,
  setSurvivalContainer,
  survivalPlotloading = false,
  uniqueClass,
  palette = [colors(0), colors(1)],
}: TProps) => {
  const { results = [], overallStats = {} } = rawData || {};
  const pValue = overallStats.pValue;

  return (
    <Loader
      loading={survivalPlotloading}
      height={height}
      className={uniqueClass}
    >
      {!survivalPlotloading &&
        <Column className="test-survival-plot-meta">
          <VisualizationHeader
            title={TITLE}
            buttons={[
              <DownloadVisualizationButton
                key="download"
                svg={() =>
                  wrapSvg({
                    selector: `.${uniqueClass} .${CLASS_NAME} svg`,
                    title: TITLE,
                    className: CLASS_NAME,
                    embed: {
                      top: {
                        elements: [
                          document.querySelector(`.${uniqueClass} .legend-0`),
                          document.querySelector(`.${uniqueClass} .legend-1`),
                          pValue
                            ? document.querySelector(`.${uniqueClass} .p-value`)
                            : null,
                        ],
                      },
                    },
                  })}
                data={results}
                stylePrefix={`.${CLASS_NAME}`}
                slug="survival-plot"
                noText
                tooltipHTML="Download SurvivalPlot data or image"
                tsvData={results.reduce((data, set) => {
                  const mapData = set.donors.map(d => toMap(d));
                  return [
                    ...data,
                    ...(results.length > 1
                      ? mapData.map(m => m.set('label', set.meta.label))
                      : mapData),
                  ];
                }, [])}
              />,
              <Tooltip Component="Reset SurvivalPlot Zoom" key="reset">
                <Button style={visualizingButton} onClick={() => setXDomain()}>
                  <Hidden>Reset</Hidden>
                </Button>
              </Tooltip>,
            ]}
          />

            <Row
              style={{
                justifyContent: 'center',
                flexWrap: 'wrap',
                marginTop: '0.5rem',
              }}
            >
              {legend &&
                legend.map((l, i) =>
                  <div className={`legend-${i}`} key={l.key}>

                      {l.value}

,
                )}
            </Row>

          {
            <Tooltip
              Component={
                pValue === 0 &&

                  Value shows 0.00e+0 because the
P-Value is extremely low
                  and goes beyond
the precision inherent in the code

              }
            >

                  {_.isNumber(pValue) &&
                    `Log-Rank Test P-Value = ${pValue.toExponential(2)}`}

            </Tooltip>
          }

            drag to zoom

        </Column>}

    </Loader>
  );
};

function renderSurvivalPlot(props: TProps): void {
  const {
    height = 0,
    rawData = {},
    xDomain,
    survivalContainer,
    setXDomain,
    setTooltip,
    push,
    palette = [colors(0), colors(1)],
  } = props;
  const { results = [] } = rawData;
  if (survivalContainer) {
    performanceTracker.begin('survival:render');
    renderPlot({
      container: survivalContainer,
      dataSets: results,
      palette,
      xDomain,
      xAxisLabel: 'Duration (days)',
      yAxisLabel: 'Survival Rate',
      height,
      getSetSymbol: (curve, curves) =>
        curves.length === 1
          ? ''
          : `<tspan font-style="italic">S</tspan><tspan font-size="0.7em" baseline-shift="-15%">${curves.indexOf(
              curve,
            ) + 1}</tspan>`,
      onMouseEnterDonor: (
        e,
        { id, survivalEstimate, time = 0, censored, submitter_id, project_id },
      ) => {
        setTooltip(

            Case ID: {project_id} / {submitter_id}

            Survival Rate: {Math.round(survivalEstimate * 100)}%

            {censored
              ? `Interval of last follow-up: ${time.toLocaleString()} days`
              : `Time of Death: ${time.toLocaleString()} days`}
          ,
        );
      },
      onMouseLeaveDonor: () => setTooltip(),
      onClickDonor: (e, donor) => push({ pathname: `/cases/${donor.id}` }),
      onDomainChange: setXDomain,
      margins: SVG_MARGINS,
    });
    const performanceContext = {
      data_sets: results.length,
      donors: _.sum(results.map(x => x.donors.length)),
    };
    performanceTracker.end('survival:render', performanceContext);
  }
}

const enhance = compose(
  withTooltip,
  withRouter,
  withState('xDomain', 'setXDomain', undefined),
  withState('survivalContainer', 'setSurvivalContainer', null),
  withState('uniqueClass', 'setUniqueClass', () => CLASS_NAME + _.uniqueId()),
  withSize(),
  lifecycle({
    shouldComponentUpdate(nextProps: TProps): void {
      const props = ['xDomain', 'size', 'rawData', 'survivalPlotloading'];
      return !_.isEqual(_.pick(this.props, props), _.pick(nextProps, props));
    },

    componentDidUpdate(): void {
      !this.props.survivalPlotloading && renderSurvivalPlot(this.props);
    },

    componentDidMount(): void {
      !this.props.survivalPlotloading && renderSurvivalPlot(this.props);
    },
  }),
);

export default enhance(SurvivalPlotWrapper);
<commit_msg>🐛 [PRTL-1637] fix survival updating<commit_after>// @flow

import React from 'react';
import { compose, lifecycle, withState } from 'recompose';
import _ from 'lodash';
import { scaleOrdinal, schemeCategory10 } from 'd3';
import { renderPlot } from '@oncojs/survivalplot';
import Loader from '@ncigdc/uikit/Loaders/Loader';
import toMap from '@ncigdc/utils/toMap';
import { Row, Column } from '@ncigdc/uikit/Flex';
import Button from '@ncigdc/uikit/Button';
import { Tooltip, withTooltip } from '@ncigdc/uikit/Tooltip';
import Hidden from '@ncigdc/components/Hidden';
import withRouter from '@ncigdc/utils/withRouter';
import { wrapSvg } from '@ncigdc/utils/wrapSvg';
import withSize from '@ncigdc/utils/withSize';
import DownloadVisualizationButton from '@ncigdc/components/DownloadVisualizationButton';
import { visualizingButton } from '@ncigdc/theme/mixins';
import VisualizationHeader from '@ncigdc/components/VisualizationHeader';
import { performanceTracker } from '@ncigdc/utils/analytics';

import './survivalPlot.css';

const CLASS_NAME = 'survival-plot';

type TProps = {
  height: number,
  legend: Array<{
    key: string,
    value: any,
  }>,
  rawData: {
    results: Array<{
      donors: Array<Object>,
      meta: {
        id: string | number,
        label: string,
      },
    }>,
    overallStats: {
      pValue: number,
    },
  },
  setXDomain: Function,
  setSurvivalContainer: Function,
  survivalPlotloading: boolean,
  xDomain: Array<number>,
  survivalContainer: Element,
  setTooltip: Function,
  push: Function,
  uniqueClass: string,
};

const TITLE = 'Overall Survival Plot';

const SVG_MARGINS = {
  top: 15,
  right: 20,
  bottom: 40,
  left: 50,
};

const colors = scaleOrdinal(schemeCategory10);

const styles = {
  pValue: {
    fontSize: '1.1rem',
    height: '1.5rem',
    marginTop: '0.5rem',
    textAlign: 'center',
  },
};

const SurvivalPlotWrapper = ({
  height = 0,
  legend,
  rawData,
  setXDomain,
  setSurvivalContainer,
  survivalPlotloading = false,
  uniqueClass,
  palette = [colors(0), colors(1)],
}: TProps) => {
  const { results = [], overallStats = {} } = rawData || {};
  const pValue = overallStats.pValue;

  return (
    <Loader
      loading={survivalPlotloading}
      height={height}
      className={uniqueClass}
    >
      {!survivalPlotloading &&
        <Column className="test-survival-plot-meta">
          <VisualizationHeader
            title={TITLE}
            buttons={[
              <DownloadVisualizationButton
                key="download"
                svg={() =>
                  wrapSvg({
                    selector: `.${uniqueClass} .${CLASS_NAME} svg`,
                    title: TITLE,
                    className: CLASS_NAME,
                    embed: {
                      top: {
                        elements: [
                          document.querySelector(`.${uniqueClass} .legend-0`),
                          document.querySelector(`.${uniqueClass} .legend-1`),
                          pValue
                            ? document.querySelector(`.${uniqueClass} .p-value`)
                            : null,
                        ],
                      },
                    },
                  })}
                data={results}
                stylePrefix={`.${CLASS_NAME}`}
                slug="survival-plot"
                noText
                tooltipHTML="Download SurvivalPlot data or image"
                tsvData={results.reduce((data, set) => {
                  const mapData = set.donors.map(d => toMap(d));
                  return [
                    ...data,
                    ...(results.length > 1
                      ? mapData.map(m => m.set('label', set.meta.label))
                      : mapData),
                  ];
                }, [])}
              />,
              <Tooltip Component="Reset SurvivalPlot Zoom" key="reset">
                <Button style={visualizingButton} onClick={() => setXDomain()}>
                  <Hidden>Reset</Hidden>
                </Button>
              </Tooltip>,
            ]}
          />

            <Row
              style={{
                justifyContent: 'center',
                flexWrap: 'wrap',
                marginTop: '0.5rem',
              }}
            >
              {legend &&
                legend.map((l, i) =>
                  <div className={`legend-${i}`} key={l.key}>

                      {l.value}

,
                )}
            </Row>

          {
            <Tooltip
              Component={
                pValue === 0 &&

                  Value shows 0.00e+0 because the
P-Value is extremely low
                  and goes beyond
the precision inherent in the code

              }
            >

                  {_.isNumber(pValue) &&
                    `Log-Rank Test P-Value = ${pValue.toExponential(2)}`}

            </Tooltip>
          }

            drag to zoom

        </Column>}

    </Loader>
  );
};

function renderSurvivalPlot(props: TProps): void {
  const {
    height = 0,
    rawData = {},
    xDomain,
    survivalContainer,
    setXDomain,
    setTooltip,
    push,
    palette = [colors(0), colors(1)],
  } = props;
  const { results = [] } = rawData;
  if (survivalContainer) {
    performanceTracker.begin('survival:render');
    renderPlot({
      container: survivalContainer,
      dataSets: results,
      palette,
      xDomain,
      xAxisLabel: 'Duration (days)',
      yAxisLabel: 'Survival Rate',
      height,
      getSetSymbol: (curve, curves) =>
        curves.length === 1
          ? ''
          : `<tspan font-style="italic">S</tspan><tspan font-size="0.7em" baseline-shift="-15%">${curves.indexOf(
              curve,
            ) + 1}</tspan>`,
      onMouseEnterDonor: (
        e,
        { id, survivalEstimate, time = 0, censored, submitter_id, project_id },
      ) => {
        setTooltip(

            Case ID: {project_id} / {submitter_id}

            Survival Rate: {Math.round(survivalEstimate * 100)}%

            {censored
              ? `Interval of last follow-up: ${time.toLocaleString()} days`
              : `Time of Death: ${time.toLocaleString()} days`}
          ,
        );
      },
      onMouseLeaveDonor: () => setTooltip(),
      onClickDonor: (e, donor) => push({ pathname: `/cases/${donor.id}` }),
      onDomainChange: setXDomain,
      margins: SVG_MARGINS,
    });
    const performanceContext = {
      data_sets: results.length,
      donors: _.sum(results.map(x => x.donors.length)),
    };
    performanceTracker.end('survival:render', performanceContext);
  }
}

const enhance = compose(
  withTooltip,
  withRouter,
  withState('xDomain', 'setXDomain', undefined),
  withState('survivalContainer', 'setSurvivalContainer', null),
  withState('uniqueClass', 'setUniqueClass', () => CLASS_NAME + _.uniqueId()),
  withSize(),
  lifecycle({
    shouldComponentUpdate(nextProps: TProps): void {
      const props = [
        'xDomain',
        'size',
        'rawData',
        'survivalPlotloading',
        'survivalContainer',
      ];
      return !_.isEqual(_.pick(this.props, props), _.pick(nextProps, props));
    },

    componentDidUpdate(): void {
      !this.props.survivalPlotloading && renderSurvivalPlot(this.props);
    },

    componentDidMount(): void {
      !this.props.survivalPlotloading && renderSurvivalPlot(this.props);
    },
  }),
);

export default enhance(SurvivalPlotWrapper);
<|endoftext|>


<commit_before>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string,      // className for the outer element
		matchPos: React.PropTypes.string,       // (any|start) match the start or entire string when filtering
		matchProp: React.PropTypes.string       // (any|label|value) which option property to filter on
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined,
			matchPos: 'any',
			matchProp: 'any'
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - filteredOptions
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this._optionsFilterString = '';
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
			this.setState({
				options: newProps.options,
				filteredOptions: this.filterOptions(newProps.options)
			});
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		// reset internal filter string
		this._optionsFilterString = '';

		var values = this.initValuesArray(value),
			filteredOptions = this.filterOptions(this.state.options, values);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			filteredOptions: filteredOptions,
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi && values.length ? values[0] : filteredOptions[0]
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event && event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {

		// assign an internal variable because we need to use
		// the latest value before setState() has completed.
		this._optionsFilterString = event.target.value;

		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			var filteredOptions = this.filterOptions(this.state.options);
			this.setState({
				isOpen: true,
				inputValue: event.target.value,
				filteredOptions: filteredOptions,
				focusedOption: _.contains(filteredOptions, this.state.focusedOption) ? this.state.focusedOption : filteredOptions[0]
			});
		}

	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				var options = this._optionsCache[cacheKey].options;
				this.setState(_.extend({
					options: options,
					filteredOptions: this.filterOptions(options)
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options,
				filteredOptions: this.filterOptions(data.options)
			}, state));

		}.bind(this));

	},

	filterOptions: function(options, values) {
		var filterValue = this._optionsFilterString;
		var exclude = (values || this.state.values).map(function(i) {
			return i.value;
		});
		var filterOption = function(op) {
			if (this.props.multi && _.contains(exclude, op.value)) return false;
			if (this.props.filterOption) return this.props.filterOption.call(this, option, filterValue);
			return !filterValue || (this.props.matchPos === 'start') ? (
				(this.props.matchProp !== 'label' && op.value.toLowerCase().substr(0, filterValue.length) === filterValue) ||
				(this.props.matchProp !== 'value' && op.label.toLowerCase().substr(0, filterValue.length) === filterValue)
			) : (
				(this.props.matchProp !== 'label' && op.value.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) ||
				(this.props.matchProp !== 'value' && op.label.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0)
			);
		}
		return _.filter(options, filterOption, this);
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.state.filteredOptions;

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;

		var ops = _.map(this.state.filteredOptions, function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': focusedValue === op.value
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops :
No results found
;

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(<Value {...props} />);
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ?  : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<commit_msg>Adding filterOption and filterOptions props<commit_after>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string,      // className for the outer element
		filterOption: React.PropTypes.func,     // method to filter a single option: function(option, filterString)
		filterOptions: React.PropTypes.func,    // method to filter the options array: function([options], filterString, [values])
		matchPos: React.PropTypes.string,       // (any|start) match the start or entire string when filtering
		matchProp: React.PropTypes.string       // (any|label|value) which option property to filter on
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined,
			matchPos: 'any',
			matchProp: 'any'
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - filteredOptions
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this._optionsFilterString = '';
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
			this.setState({
				options: newProps.options,
				filteredOptions: this.filterOptions(newProps.options)
			});
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		// reset internal filter string
		this._optionsFilterString = '';

		var values = this.initValuesArray(value),
			filteredOptions = this.filterOptions(this.state.options, values);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			filteredOptions: filteredOptions,
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi && values.length ? values[0] : filteredOptions[0]
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event && event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {

		// assign an internal variable because we need to use
		// the latest value before setState() has completed.
		this._optionsFilterString = event.target.value;

		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			var filteredOptions = this.filterOptions(this.state.options);
			this.setState({
				isOpen: true,
				inputValue: event.target.value,
				filteredOptions: filteredOptions,
				focusedOption: _.contains(filteredOptions, this.state.focusedOption) ? this.state.focusedOption : filteredOptions[0]
			});
		}

	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				var options = this._optionsCache[cacheKey].options;
				this.setState(_.extend({
					options: options,
					filteredOptions: this.filterOptions(options)
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options,
				filteredOptions: this.filterOptions(data.options)
			}, state));

		}.bind(this));

	},

	filterOptions: function(options, values) {
		var filterValue = this._optionsFilterString;
		var exclude = (values || this.state.values).map(function(i) {
			return i.value;
		});
		if (this.props.filterOptions) {
			return this.props.filterOptions.call(this, options, filterValue, exclude);
		} else {
			var filterOption = function(op) {
				if (this.props.multi && _.contains(exclude, op.value)) return false;
				if (this.props.filterOption) return this.props.filterOption.call(this, op, filterValue);
				return !filterValue || (this.props.matchPos === 'start') ? (
					(this.props.matchProp !== 'label' && op.value.toLowerCase().substr(0, filterValue.length) === filterValue) ||
					(this.props.matchProp !== 'value' && op.label.toLowerCase().substr(0, filterValue.length) === filterValue)
				) : (
					(this.props.matchProp !== 'label' && op.value.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) ||
					(this.props.matchProp !== 'value' && op.label.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0)
				);
			}
			return _.filter(options, filterOption, this);
		}
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.state.filteredOptions;

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;

		var ops = _.map(this.state.filteredOptions, function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': focusedValue === op.value
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops :
No results found
;

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(<Value {...props} />);
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ?  : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<|endoftext|>


<commit_before><commit_msg>Add wikipedia title cleanup<commit_after><|endoftext|>