<commit_before>/* ************************************************************************

    License: MIT Licence

    Dependencies : [
        a.js
        core/message.js
    ]

    Events : [
        init: {}
    ]

    Description:
        Provide a model based system to create and manage models threw
        application lifetime

************************************************************************ */

/*
TODO:
  Check doit pouvoir accepter un tableau

  Accepter un pattern comme validation d'un élément du model
*/

/*
 * Property available element :
 *   - nullable {Boolean}   if the property can be set to null or not
 *   - init {Mixed}         the initial value
 *   - needed {Boolean}     Indicate if the property should ALWAYS be
 *                          included when performing a save to server
 *   - check {String}       the typeof check (like String, Object, ...)
 *   - pattern {String}     the regex pattern to check
 *   - validate {Function}  the function to use for validate input.
 *                          Must return true and false value to validate or not
 *   - transform {Function} the transformation to apply before setting data
 *   - event {String}       the event to raise on any change
 *   - apply {Function}     the apply element
*/

/**
 * A model creator to manage your model type.
 *
 * @function model
 * @namespace a
 *
 * @param name {String}                     The model name to create
 * @param properties {Object}               The properties associated to the
 *                                          model.
 * @param requests {Object}                 The server side associated requests
 *                                          to manipulate the model, save it...
*/
a.model = function(name, properties, requests) {
    // Only allow new name (already existing name just give already existing
    // model definition)
    if(!a.modelPooler.get(name)) {
        a.modelPooler.set(name, {
            properties: properties,
            requests: requests
        });
    }

    return function() {
        return a.modelPooler.createInstance(name);
    };
};

/**
 * A model manager helps to keep a trace of every model currently used by the
 * application.
 *
 * @class modelManager
 * @namespace a
 * @constructor
*/
a.modelManager = {
    /**
     * Store a pointer to every instance of every model created.
     * @property _store
     * @type Object
     * @default {}
    */
    _store: a.mem.getInstance('app.model.instance'),

    /**
     * Store a new model into the modelManager.
     *
     * @method set
     *
     * @param model {Object}                The new model to store
    */
    set: function(model) {
        this._store.set(model.uid, model);
    },

    /**
     * Get a model from it's uid (the unique id is automatically generated
     * for every model, it's available threw myModelInstance.uid)
     *
     * @method get
     *
     * @param uid {Integer}                 The unique id to search related
     *                                      model from
     * @return {Object | null}              The related model found, or null if
     *                                      nothing is found
    */
    get: function(uid) {
        return this._store.get(uid);
    },

    /**
     * Remove a model from store.
     *
     * @method remove
     *
     * @param uid {Integer}                 The uid to remove
    */
    remove: function(uid) {
        this._store.remove(uid);
    },

    /**
     * Get all models related to a given namespace. For example, if you create
     * a.model('user'), this function helps to find all *user* model created.
     *
     * @method getByName
     *
     * @param name {String}                 The model name to find
     * @return {Array}                      The array with all model instance
     *                                      related to this name
    */
    getByName: function(name) {
        var result = [];

        a.each(this._store.list(), function(element) {
            if(element.name === name) {
                result.push(element);
            }
        });

        return result;
    }
};

/**
 * A model pooler aims to create a storage space to keep every model type
 * existing.
 *
 * @class modelPooler
 * @namespace a
 * @constructor
*/
a.modelPooler = a.mem.getInstance('app.model.type');

/**
 * Simple function to generate new instance from a base
 *
 * @method createInstance
 *
 * @param name {String}                     The model type we want to create
 * @return {Object | null}                  The model instance created, or null
 *                                          if model name is not defined
*/
a.modelPooler.createInstance = function(name) {
    var instanceType = this.get(name);

    if(!instanceType) {
        return null;
    }

    var model = a.extend(
            new a.modelInstance(
                name,
                a.clone(instanceType.properties),
                a.clone(instanceType.requests)
            ),
            new a.eventEmitter('a.model')
        );

    // Resetting model
    model.init();

    // Adding model to modelManager system
    a.modelManager.set(model);

    // Returning freshly created model
    return model;
};

/**
 * Delete an existing instance.
 *
 * @method deleteInstance
 *
 * @param instance {Object}                 The instance to delete
*/
a.modelPooler.deleteInstance = function(instance) {
    if(a.isTrueObject(instance) && instance.uid) {
        a.modelManager.remove(instance.uid);
    }
};

/**
 * A model instance generator to manage multiple instance from a main model.
 * NEVER USE BY ITSELF, you should always go threw a.model before.
 *
 * @class modelInstance
 * @namespace a
 * @constructor
 *
 * @param name {String}                     The model name to create
 * @param properties {Object}               The properties associated to the
 *                                          model.
 * @param requests {Object}                 The server side associated requests
 *                                          to manipulate the model, save it...
*/
a.modelInstance = function(name, properties, requests) {
    this.name = name || '';
    this.properties = {};
    this.snapshot   = {};
    this.requests   = {};

    // Internal unique id tracer
    this.uid        = a.uniqueId();
    this.nid        = name + '-' + this.uid;

    if(a.isTrueObject(properties)) {
        this.properties = properties;
    }

    if(a.isTrueObject(requests)) {
        this.requests = requests;
    }
}

a.modelInstance.prototype = {
    /**
     * Get a single property value.
     *
     * @method get
     *
     * @param key {String}                  The property key
     * @return {Object}                     The property value or null if not
     *                                      existing
    */
    get: function(key) {
        var p = this.properties[key];
        return p ? p['value'] : null;
    },

    /**
     * Get the property list stored in the model.
     *
     * @method list
     *
     * @return {Object}                     The property list currently setted
    */
    list: function() {
        var properties = [];
        for(var i in this.properties) {
            properties.push(i);
        }
        return properties;
    },

    /**
     * Set the given property value.
     *
     * @method set
     *
     * @param key {String}                  The property key
     * @param value {Object}                The property value
    */
    set: function(key, value) {
        var property = this.properties[key];

        // If the property is setted, we can use it
        if(property) {
            var check     = property['check'],
                apply     = property['apply'],
                eventName = property['event'],
                transform = property['transform'],
                validate  = property['validate'],
                old       = property['value'];

            // TRANSFORM
            value = a.isFunction(transform) ? transform(value, old) : value;

            // NULLABLE TEST || CHECK TEST || VALIDATE TEST
            if(
                    ( property['nullable'] === false && a.isNone(value) )
                ||  ( a.isString(check) && check.toLowerCase() !== typeof(value) )
                ||  (a.isFunction(validate) && validate(value, old) !== true)
            ) {
                return;
            }

            // We can apply property value now
            property['value'] = value;

            // APPLY TEST
            if(a.isFunction(apply)) {
                apply(value, old);
            }

            if(eventName) {
                this.dispatch(eventName, {
                    value: value,
                    old: old
                });
            }
        }
    },

    /**
     * Check if a given key exist or not in model.
     *
     * @method has
     *
     * @param key {String}                  The key to test
    */
    has: function(key) {
        return key in this.properties;
    },

    /**
     * Clear model (rollback to default values for all properties)
     *
     * @method init
    */
    init: function() {
        for(var property in this.properties) {
            this.properties[property]['value'] =
                    this.properties[property]['init'] || null;
        }

        // Save current setted data
        this.takeSnapshot();

        this.dispatch('init', {});
    },

    /**
     * Convert model to JSON data
     *
     * @method toJSON
     *
     * @return {Object}                     The serialized JSON model
    */
    toJSON: function() {
        var json = {};
        for(var property in this.properties) {
            json[property] = this.get(property);
        }
        return a.parser.json.stringify(json);
    },

    /**
     * From a JSON, fill a JSON instance
     *
     * @method fromJSON
     *
     * @param data {Object}                 The input JSON data
    */
    fromJSON: function(data) {
        if(a.isString(data) && data.length > 0) {
            data = a.parser.json.parse(data);
        }
        for(var property in this.properties) {
            if(property in data) {
                this.properties[property]['value'] = data[property];
            }
        }
    },

    /**
     * Take a model snapshot.
     *
     * @method takeSnapshot
     *
     * @return {Object}                     The snapshot created
    */
    takeSnapshot: function() {
        this.snapshot = {};
        for(var property in this.properties) {
            this.snapshot[property] = this.get(property);
        }
        return this.getSnapshot();
    },

    /**
     * Get the current stored snapshot.
     *
     * @method getSnapshot
     *
     * @return {Object}                     The snapshot currently stored
    */
    getSnapshot: function() {
        return this.snapshot;
    },

    /**
     * From the latest takeSnapshot used, retrieve the properties value
     * difference.
     * It helps to send to server only modified informations since last
     * snapshot.
     *
     * @method differenceSnapshot
     *
     * @param onlyCurrentValues {Boolean}   By default every properties found
     *                                      got a couple {value/old} object.
     *                                      But sometimes you may prefer to get
     *                                      only the current value and not old
     *                                      one, this parameter is for that.
     *                                      (default: false)
     * @return {Object}                     The difference between old and
     *                                      current model state
    */
    differenceSnapshot: function(onlyCurrentValues) {
        var snapshot   = this.snapshot,
            properties = this.properties,
            difference = {};

        for(var key in snapshot) {
            var snapValue    = snapshot[key],
                currentValue = properties[key]['value'];

            // Validate on value change, or needed stuff
            if(
                    currentValue !== snapValue
                ||  properties[key]['needed'] === true
            ) {
                if(onlyCurrentValues) {
                    difference[key] = currentValue;
                } else {
                    difference[key] = {
                        value: currentValue,
                        old:   snapValue
                    };
                }
            }
        }

        return difference;
    },

    /**
     * Get one of the associated model request.
     *
     * @param request
     *
     * @param name {String}                 The request name to get
    */
    request: function(name) {
        // TODO: here we take the existing request, and create a ready to use
        // object
        var input  = this.requests[name] || {},
            output = {type: 'json'};

        output.method = input.method;
        output.url    = input.url;

        return output;
    }
};

/*
------------------------------
  HANDLEBARS HELPERS
------------------------------
*/
(function() {
    // From a given uid, get the linked model
    a.parameter.addParameterType('model',  function(uid) {
        return a.modelManager.get(uid);
    });

    // This helps to get model uid from a given model
    // The idea behind this is to recieve a model in parameter and lets
    // get the uid for form plugin
    Handlebars.registerHelper('model', function(object) {
        if(a.isString(object) || a.isNumber(object)) {
            return object;
        } else if(a.isTrueObject(object) && object.uid) {
            return object.uid;
        }
        return null;
    });
})();<commit_msg>Adding clear method to model manager<commit_after>/* ************************************************************************

    License: MIT Licence

    Dependencies : [
        a.js
        core/message.js
    ]

    Events : [
        init: {}
    ]

    Description:
        Provide a model based system to create and manage models threw
        application lifetime

************************************************************************ */

/*
TODO:
  Check doit pouvoir accepter un tableau

  Accepter un pattern comme validation d'un élément du model
*/

/*
 * Property available element :
 *   - nullable {Boolean}   if the property can be set to null or not
 *   - init {Mixed}         the initial value
 *   - needed {Boolean}     Indicate if the property should ALWAYS be
 *                          included when performing a save to server
 *   - check {String}       the typeof check (like String, Object, ...)
 *   - pattern {String}     the regex pattern to check
 *   - validate {Function}  the function to use for validate input.
 *                          Must return true and false value to validate or not
 *   - transform {Function} the transformation to apply before setting data
 *   - event {String}       the event to raise on any change
 *   - apply {Function}     the apply element
*/

/**
 * A model creator to manage your model type.
 *
 * @function model
 * @namespace a
 *
 * @param name {String}                     The model name to create
 * @param properties {Object}               The properties associated to the
 *                                          model.
 * @param requests {Object}                 The server side associated requests
 *                                          to manipulate the model, save it...
*/
a.model = function(name, properties, requests) {
    // Only allow new name (already existing name just give already existing
    // model definition)
    if(!a.modelPooler.get(name)) {
        a.modelPooler.set(name, {
            properties: properties,
            requests: requests
        });
    }

    return function() {
        return a.modelPooler.createInstance(name);
    };
};

/**
 * A model manager helps to keep a trace of every model currently used by the
 * application.
 *
 * @class modelManager
 * @namespace a
 * @constructor
*/
a.modelManager = {
    /**
     * Store a pointer to every instance of every model created.
     * @property _store
     * @type Object
     * @default {}
    */
    _store: a.mem.getInstance('app.model.instance'),

    /**
     * Store a new model into the modelManager.
     *
     * @method set
     *
     * @param model {Object}                The new model to store
    */
    set: function(model) {
        this._store.set(model.uid, model);
    },

    /**
     * Get a model from it's uid (the unique id is automatically generated
     * for every model, it's available threw myModelInstance.uid)
     *
     * @method get
     *
     * @param uid {Integer}                 The unique id to search related
     *                                      model from
     * @return {Object | null}              The related model found, or null if
     *                                      nothing is found
    */
    get: function(uid) {
        return this._store.get(uid);
    },

    /**
     * Remove a model from store.
     *
     * @method remove
     *
     * @param uid {Integer}                 The uid to remove
    */
    remove: function(uid) {
        this._store.remove(uid);
    },

    /**
     * Remove all existing model from store
     *
     * @method clear
    */
    clear: function() {
        this._store.clear();
    },

    /**
     * Get all models related to a given namespace. For example, if you create
     * a.model('user'), this function helps to find all *user* model created.
     *
     * @method getByName
     *
     * @param name {String}                 The model name to find
     * @return {Array}                      The array with all model instance
     *                                      related to this name
    */
    getByName: function(name) {
        var result = [];

        a.each(this._store.list(), function(element) {
            if(element.name === name) {
                result.push(element);
            }
        });

        return result;
    }
};

/**
 * A model pooler aims to create a storage space to keep every model type
 * existing.
 *
 * @class modelPooler
 * @namespace a
 * @constructor
*/
a.modelPooler = a.mem.getInstance('app.model.type');

/**
 * Simple function to generate new instance from a base
 *
 * @method createInstance
 *
 * @param name {String}                     The model type we want to create
 * @return {Object | null}                  The model instance created, or null
 *                                          if model name is not defined
*/
a.modelPooler.createInstance = function(name) {
    var instanceType = this.get(name);

    if(!instanceType) {
        return null;
    }

    var model = a.extend(
            new a.modelInstance(
                name,
                a.clone(instanceType.properties),
                a.clone(instanceType.requests)
            ),
            new a.eventEmitter('a.model')
        );

    // Resetting model
    model.init();

    // Adding model to modelManager system
    a.modelManager.set(model);

    // Returning freshly created model
    return model;
};

/**
 * Delete an existing instance.
 *
 * @method deleteInstance
 *
 * @param instance {Object}                 The instance to delete
*/
a.modelPooler.deleteInstance = function(instance) {
    if(a.isTrueObject(instance) && instance.uid) {
        a.modelManager.remove(instance.uid);
    }
};

/**
 * A model instance generator to manage multiple instance from a main model.
 * NEVER USE BY ITSELF, you should always go threw a.model before.
 *
 * @class modelInstance
 * @namespace a
 * @constructor
 *
 * @param name {String}                     The model name to create
 * @param properties {Object}               The properties associated to the
 *                                          model.
 * @param requests {Object}                 The server side associated requests
 *                                          to manipulate the model, save it...
*/
a.modelInstance = function(name, properties, requests) {
    this.name = name || '';
    this.properties = {};
    this.snapshot   = {};
    this.requests   = {};

    // Internal unique id tracer
    this.uid        = a.uniqueId();
    this.nid        = name + '-' + this.uid;

    if(a.isTrueObject(properties)) {
        this.properties = properties;
    }

    if(a.isTrueObject(requests)) {
        this.requests = requests;
    }
}

a.modelInstance.prototype = {
    /**
     * Get a single property value.
     *
     * @method get
     *
     * @param key {String}                  The property key
     * @return {Object}                     The property value or null if not
     *                                      existing
    */
    get: function(key) {
        var p = this.properties[key];
        return p ? p['value'] : null;
    },

    /**
     * Get the property list stored in the model.
     *
     * @method list
     *
     * @return {Object}                     The property list currently setted
    */
    list: function() {
        var properties = [];
        for(var i in this.properties) {
            properties.push(i);
        }
        return properties;
    },

    /**
     * Set the given property value.
     *
     * @method set
     *
     * @param key {String}                  The property key
     * @param value {Object}                The property value
    */
    set: function(key, value) {
        var property = this.properties[key];

        // If the property is setted, we can use it
        if(property) {
            var check     = property['check'],
                apply     = property['apply'],
                eventName = property['event'],
                transform = property['transform'],
                validate  = property['validate'],
                old       = property['value'];

            // TRANSFORM
            value = a.isFunction(transform) ? transform(value, old) : value;

            // NULLABLE TEST || CHECK TEST || VALIDATE TEST
            if(
                    ( property['nullable'] === false && a.isNone(value) )
                ||  ( a.isString(check) && check.toLowerCase() !== typeof(value) )
                ||  (a.isFunction(validate) && validate(value, old) !== true)
            ) {
                return;
            }

            // We can apply property value now
            property['value'] = value;

            // APPLY TEST
            if(a.isFunction(apply)) {
                apply(value, old);
            }

            if(eventName) {
                this.dispatch(eventName, {
                    value: value,
                    old: old
                });
            }
        }
    },

    /**
     * Check if a given key exist or not in model.
     *
     * @method has
     *
     * @param key {String}                  The key to test
    */
    has: function(key) {
        return key in this.properties;
    },

    /**
     * Clear model (rollback to default values for all properties)
     *
     * @method init
    */
    init: function() {
        for(var property in this.properties) {
            this.properties[property]['value'] =
                    this.properties[property]['init'] || null;
        }

        // Save current setted data
        this.takeSnapshot();

        this.dispatch('init', {});
    },

    /**
     * Convert model to JSON data
     *
     * @method toJSON
     *
     * @return {Object}                     The serialized JSON model
    */
    toJSON: function() {
        var json = {};
        for(var property in this.properties) {
            json[property] = this.get(property);
        }
        return a.parser.json.stringify(json);
    },

    /**
     * From a JSON, fill a JSON instance
     *
     * @method fromJSON
     *
     * @param data {Object}                 The input JSON data
    */
    fromJSON: function(data) {
        if(a.isString(data) && data.length > 0) {
            data = a.parser.json.parse(data);
        }
        for(var property in this.properties) {
            if(property in data) {
                this.properties[property]['value'] = data[property];
            }
        }
    },

    /**
     * Take a model snapshot.
     *
     * @method takeSnapshot
     *
     * @return {Object}                     The snapshot created
    */
    takeSnapshot: function() {
        this.snapshot = {};
        for(var property in this.properties) {
            this.snapshot[property] = this.get(property);
        }
        return this.getSnapshot();
    },

    /**
     * Get the current stored snapshot.
     *
     * @method getSnapshot
     *
     * @return {Object}                     The snapshot currently stored
    */
    getSnapshot: function() {
        return this.snapshot;
    },

    /**
     * From the latest takeSnapshot used, retrieve the properties value
     * difference.
     * It helps to send to server only modified informations since last
     * snapshot.
     *
     * @method differenceSnapshot
     *
     * @param onlyCurrentValues {Boolean}   By default every properties found
     *                                      got a couple {value/old} object.
     *                                      But sometimes you may prefer to get
     *                                      only the current value and not old
     *                                      one, this parameter is for that.
     *                                      (default: false)
     * @return {Object}                     The difference between old and
     *                                      current model state
    */
    differenceSnapshot: function(onlyCurrentValues) {
        var snapshot   = this.snapshot,
            properties = this.properties,
            difference = {};

        for(var key in snapshot) {
            var snapValue    = snapshot[key],
                currentValue = properties[key]['value'];

            // Validate on value change, or needed stuff
            if(
                    currentValue !== snapValue
                ||  properties[key]['needed'] === true
            ) {
                if(onlyCurrentValues) {
                    difference[key] = currentValue;
                } else {
                    difference[key] = {
                        value: currentValue,
                        old:   snapValue
                    };
                }
            }
        }

        return difference;
    },

    /**
     * Get one of the associated model request.
     *
     * @param request
     *
     * @param name {String}                 The request name to get
    */
    request: function(name) {
        // TODO: here we take the existing request, and create a ready to use
        // object
        var input  = this.requests[name] || {},
            output = {type: 'json'};

        output.method = input.method;
        output.url    = input.url;

        return output;
    }
};

/*
------------------------------
  HANDLEBARS HELPERS
------------------------------
*/
(function() {
    // From a given uid, get the linked model
    a.parameter.addParameterType('model',  function(uid) {
        return a.modelManager.get(uid);
    });

    // This helps to get model uid from a given model
    // The idea behind this is to recieve a model in parameter and lets
    // get the uid for form plugin
    Handlebars.registerHelper('model', function(object) {
        if(a.isString(object) || a.isNumber(object)) {
            return object;
        } else if(a.isTrueObject(object) && object.uid) {
            return object.uid;
        }
        return null;
    });
})();<|endoftext|>


<commit_before>import React from 'react'
import { Link } from 'react-router-dom'
import {Button, DropdownButton, MenuItem, Pagination, OverlayTrigger, Popover} from 'react-bootstrap'

var axios = require('axios');

{/* Responsible for all styling on the page */}
var panelColor = {
    backgroundColor: 'black',
    borderColor: 'white',
}

var linkColor = {
    color: 'white',
}

var dropdownStyle = {
    margin: '10px',
    backgroundColor: '#2b2b2b',
    borderColor: '#2b2b2b',
    color: 'white',
}

{/* Used to split the movies data so there is 3 per row */}
function splitarray(input, spacing) {
    var output = [];

    for (var i = 0; i < input.length; i += spacing) {
        output[output.length] = input.slice(i, i + spacing);
    }
    return output;
}

class Movies extends React.Component {
    constructor(props) {
        super(props);

        this.handleSelectSort = this.handleSelectSort.bind(this);
        this.handleSelectDirection = this.handleSelectDirection.bind(this);
        this.handleSelect = this.handleSelect.bind(this);
        this.handleSelectFilter = this.handleSelectFilter.bind(this);
        this.handleResetFilter = this.handleResetFilter.bind(this);
        this.updateItems = this.updateItems.bind(this);

        this.state = this.getInitialState();
        this.updateItems();
    }

    getInitialState() {
        return {
            movies: [],
            moviesGrouped: [],
            numPages: 1,
            activePage: 1,
            resultsPerPage: 6,
            orderBy: 'name',
            orderDirection: 'asc',
            q: {
                'order_by': [{"field": "title", "direction": "asc"}],
                'filters': [{"name": "poster_path", "op": "is_not_null"}]
            }
        };
    }

    //* Rerenders/updates the page to get the new data triggered by pagination, sorting, etc */
    updateItems() {
        axios.get('http://marvelus.me/api/movie', {
            params: {
                results_per_page: this.state.resultsPerPage,
                page: this.state.activePage,
                q: JSON.stringify(this.state.q),
            }
        }).then(res => {
            this.state.numPages = res.data.total_pages;
            const movies = res.data.objects.map(movie => movie);
            const moviesGrouped = splitarray(movies, 3)
            this.setState({moviesGrouped});
        });
    }

    //* When you select a page in the pagination bar */
    handleSelect(eventKey) {
        this.state.activePage = eventKey;
        this.updateItems();
    }

    //* Select how to sort (what attributes) the actors */
    handleSelectSort(eventKey) {
        this.state.q.order_by[0].field = eventKey;
        this.updateItems()
    }

    /* Select which way to sort the attributes (asc/desc) */
    handleSelectDirection(eventKey) {
        this.state.q.order_by[0].direction = eventKey;
        this.updateItems();
    }

    /* Select which filter to use */
    handleSelectFilter(eventKey) {
        this.state.q.filters.push({"name": eventKey, "op": "is_not_null"});
        this.updateItems();
    }

    /* Resets all options to the way when user first came to site */v
    handleResetFilter() {
        this.state.q.filters = [{"name": "poster_path", "op": "is_not_null"}];
        this.updateItems();
    }

    /* Displays the "sort by" dropdown */
    renderDropdownButtonSortby(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} key={"sort"} id={'dropdown-basic-${i}'}
                            onSelect={this.handleSelectSort}>
                <MenuItem eventKey="name">Name</MenuItem>
                <MenuItem eventKey="birthday">Birthday</MenuItem>

            </DropdownButton>
        );
    }

    /* Displays the "filter" dropdown */
    renderDropdownButtonFilter(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} key={"filter"} id={'dropdown-basic-${i}'}
                            onSelect={this.handleSelectFilter}>
                <MenuItem eventKey="desc">Has Description</MenuItem>
                <MenuItem eventKey="birthday">Appears In TV Show(s)</MenuItem>

            </DropdownButton>
        );
    }

    /* Displays the "order" dropdown */
    renderDropdownButtonSortDirection(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} onSelect={this.handleSelectDirection}>
                <MenuItem eventKey="asc">Ascending</MenuItem>
                <MenuItem eventKey="desc">Descending</MenuItem>
            </DropdownButton>
        );
    }

    /* Displays the "reset filter" button */
    renderResetFilterButton(title) {
        return (
            <Button style={dropdownStyle} title={title} onClick={this.handleResetFilter}>Reset Filter
            </Button>
        );
    }

    render() {
        return (

                    {/* Display all sorting, filtering, searching options */}

                        {this.renderDropdownButtonSortby("Sort By: ", "name")}
                        {this.renderDropdownButtonSortDirection("Order", "")}
                        {this.renderDropdownButtonFilter("Filter", "")}
                        {this.renderResetFilterButton("Filter")}

                {/* Go through and display 6 movies per page */}
                {this.state.moviesGrouped.length == 0 || !this.state.moviesGrouped ? null :
                    this.state.moviesGrouped.map(moviesList =>
                        !moviesList ? null :

                        {moviesList.map(movie =>

                                <Link to={"/movie/" + movie.id}>

{movie.title}

                                         /* In charge of the popover when you hover over the movies's picture */
                                         <OverlayTrigger trigger={['hover', 'focus']} placement="left" overlay={<Popover id="popover-trigger-hover-focus">
                                               Name:

                                               {movie.name}

                                               Character(s):

                                               {movie.characters.length > 0 ? movie.characters.map(function (character) {
                                                    return (character.name)
                                                }) : "None"}

                                            </Popover>}>

                                        </OverlayTrigger>

                                            Marvel Characters: {movie.characters.length}

                                </Link>

                        )}

)

                }

                {/* Display the pagination bar */}

                {!this.state.numPages
                    ? null
                    : <Pagination
                        bsSize='large'
                        prev
                        next
                        first
                        last
                        ellipsis
                        boundaryLinks
                        items={this.state.numPages}
                        maxButtons={10}
                        activePage={this.state.activePage}
                        onSelect={this.handleSelect}/>
                }

        );
    }

}

export default Movies
<commit_msg>Fixed comment<commit_after>import React from 'react'
import {Link} from 'react-router-dom'
import {Button, DropdownButton, MenuItem, OverlayTrigger, Pagination, Popover} from 'react-bootstrap'

var axios = require('axios');

{/* Responsible for all styling on the page */}
var panelColor = {
    backgroundColor: 'black',
    borderColor: 'white',
}

var linkColor = {
    color: 'white',
}

var dropdownStyle = {
    margin: '10px',
    backgroundColor: '#2b2b2b',
    borderColor: '#2b2b2b',
    color: 'white',
}

{/* Used to split the movies data so there is 3 per row */}
function splitarray(input, spacing) {
    var output = [];

    for (var i = 0; i < input.length; i += spacing) {
        output[output.length] = input.slice(i, i + spacing);
    }
    return output;
}

class Movies extends React.Component {
    constructor(props) {
        super(props);

        this.handleSelectSort = this.handleSelectSort.bind(this);
        this.handleSelectDirection = this.handleSelectDirection.bind(this);
        this.handleSelect = this.handleSelect.bind(this);
        this.handleSelectFilter = this.handleSelectFilter.bind(this);
        this.handleResetFilter = this.handleResetFilter.bind(this);
        this.updateItems = this.updateItems.bind(this);

        this.state = this.getInitialState();
        this.updateItems();
    }

    getInitialState() {
        return {
            movies: [],
            moviesGrouped: [],
            numPages: 1,
            activePage: 1,
            resultsPerPage: 6,
            orderBy: 'name',
            orderDirection: 'asc',
            q: {
                'order_by': [{"field": "title", "direction": "asc"}],
                'filters': [{"name": "poster_path", "op": "is_not_null"}]
            }
        };
    }

    //* Rerenders/updates the page to get the new data triggered by pagination, sorting, etc */
    updateItems() {
        axios.get('http://marvelus.me/api/movie', {
            params: {
                results_per_page: this.state.resultsPerPage,
                page: this.state.activePage,
                q: JSON.stringify(this.state.q),
            }
        }).then(res => {
            this.state.numPages = res.data.total_pages;
            const movies = res.data.objects.map(movie => movie);
            const moviesGrouped = splitarray(movies, 3)
            this.setState({moviesGrouped});
        });
    }

    //* When you select a page in the pagination bar */
    handleSelect(eventKey) {
        this.state.activePage = eventKey;
        this.updateItems();
    }

    //* Select how to sort (what attributes) the actors */
    handleSelectSort(eventKey) {
        this.state.q.order_by[0].field = eventKey;
        this.updateItems()
    }

    /* Select which way to sort the attributes (asc/desc) */
    handleSelectDirection(eventKey) {
        this.state.q.order_by[0].direction = eventKey;
        this.updateItems();
    }

    /* Select which filter to use */
    handleSelectFilter(eventKey) {
        this.state.q.filters.push({"name": eventKey, "op": "is_not_null"});
        this.updateItems();
    }

    /* Resets all options to the way when user first came to site */v
    handleResetFilter() {
        this.state.q.filters = [{"name": "poster_path", "op": "is_not_null"}];
        this.updateItems();
    }

    /* Displays the "sort by" dropdown */
    renderDropdownButtonSortby(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} key={"sort"} id={'dropdown-basic-${i}'}
                            onSelect={this.handleSelectSort}>
                <MenuItem eventKey="name">Name</MenuItem>
                <MenuItem eventKey="birthday">Birthday</MenuItem>

            </DropdownButton>
        );
    }

    /* Displays the "filter" dropdown */
    renderDropdownButtonFilter(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} key={"filter"} id={'dropdown-basic-${i}'}
                            onSelect={this.handleSelectFilter}>
                <MenuItem eventKey="desc">Has Description</MenuItem>
                <MenuItem eventKey="birthday">Appears In TV Show(s)</MenuItem>

            </DropdownButton>
        );
    }

    /* Displays the "order" dropdown */
    renderDropdownButtonSortDirection(title, i) {
        return (
            <DropdownButton style={dropdownStyle} title={title} onSelect={this.handleSelectDirection}>
                <MenuItem eventKey="asc">Ascending</MenuItem>
                <MenuItem eventKey="desc">Descending</MenuItem>
            </DropdownButton>
        );
    }

    /* Displays the "reset filter" button */
    renderResetFilterButton(title) {
        return (
            <Button style={dropdownStyle} title={title} onClick={this.handleResetFilter}>Reset Filter
            </Button>
        );
    }

    render() {
        return (

                    {/* Display all sorting, filtering, searching options */}

                        {this.renderDropdownButtonSortby("Sort By: ", "name")}
                        {this.renderDropdownButtonSortDirection("Order", "")}
                        {this.renderDropdownButtonFilter("Filter", "")}
                        {this.renderResetFilterButton("Filter")}

                {/* Go through and display 6 movies per page */}
                {this.state.moviesGrouped.length == 0 || !this.state.moviesGrouped ? null :
                    this.state.moviesGrouped.map(moviesList =>
                        !moviesList ? null :

                        {moviesList.map(movie =>

                                <Link to={"/movie/" + movie.id}>

{movie.title}

                                        {/* In charge of the popover when you hover over the movies's picture */}
                                         <OverlayTrigger trigger={['hover', 'focus']} placement="left" overlay={<Popover id="popover-trigger-hover-focus">
                                               Name:

                                               {movie.name}

                                               Character(s):

                                               {movie.characters.length > 0 ? movie.characters.map(function (character) {
                                                    return (character.name)
                                                }) : "None"}

                                            </Popover>}>

                                        </OverlayTrigger>

                                            Marvel Characters: {movie.characters.length}

                                </Link>

                        )}

)

                }

                {/* Display the pagination bar */}

                {!this.state.numPages
                    ? null
                    : <Pagination
                        bsSize='large'
                        prev
                        next
                        first
                        last
                        ellipsis
                        boundaryLinks
                        items={this.state.numPages}
                        maxButtons={10}
                        activePage={this.state.activePage}
                        onSelect={this.handleSelect}/>
                }

        );
    }

}

export default Movies
<|endoftext|>


<commit_before>BJ.Object = function(object, clone) {
	function __clone(obj) {
		var cloner = new Object();
		for(var i in obj) {
			if (obj.hasOwnProperty(i)) {
				cloner[i] = obj[i];
			}
		}
		return cloner;
	}

	function __equals(obj1, obj2) {
		if(obj1 == null && obj2 == null) return true;
		if(obj1 == null || obj2 == null) return false;

		var ret = true;
		for(var i in obj1) {
			var _type = typeof obj1[i];
			if (obj1.hasOwnProperty(i) && _type !== "function") {
				if(_type == "object") {
					if(!__equals(obj1[i], obj2[i])) {
						ret = false;
						break;
					}
				} else {
					if(obj1[i] !== obj2[i]) {
						ret = false;
						break;
					}
				}
			}
		}
		return ret;
	}

	var _obj;
	if(object != null) {
		if(clone === true) {
			_obj = __clone(object);
		} else {
			_obj = object;
		}
	} else {
		_obj = new Object();
	}

	_obj.equals = function(obj) {
		return __equals(_obj, obj);
	}

	_obj.same = function(obj) {
		return __equals(_obj, obj) && __equals(obj, _obj);
	}

	_obj.clone = function() {
		var obj = __clone(_obj);
		return new BJ.Object(obj);
	}

	return _obj;
}<commit_msg>deep clone<commit_after>BJ.Object = function(object, clone) {
	function __clone(obj) {
		var cloner = new Object();
		for(var i in obj) {
			if (obj.hasOwnProperty(i)) {
				if(typeof obj[i] == "object") {
					cloner[i] = __clone(obj[i]);
				} else {
					cloner[i] = obj[i];
				}
			}
		}
		return cloner;
	}

	function __equals(obj1, obj2) {
		if(obj1 == null && obj2 == null) return true;
		if(obj1 == null || obj2 == null) return false;

		var ret = true;
		for(var i in obj1) {
			var _type = typeof obj1[i];
			if (obj1.hasOwnProperty(i) && _type !== "function") {
				if(_type == "object") {
					if(!__equals(obj1[i], obj2[i])) {
						ret = false;
						break;
					}
				} else {
					if(obj1[i] !== obj2[i]) {
						ret = false;
						break;
					}
				}
			}
		}
		return ret;
	}

	var _obj;
	if(object != null) {
		if(clone === true) {
			_obj = __clone(object);
		} else {
			_obj = object;
		}
	} else {
		_obj = new Object();
	}

	_obj.equals = function(obj) {
		return __equals(_obj, obj);
	}

	_obj.same = function(obj) {
		return __equals(_obj, obj) && __equals(obj, _obj);
	}

	_obj.clone = function() {
		var obj = __clone(_obj);
		return new BJ.Object(obj);
	}

	return _obj;
}<|endoftext|>


<commit_before>/* @flow */
import {
    updateListenOptions, nameIsValid,
    getDefaultButtons, getDefaultSticks, parseGamepad,
    stickMap, buttonMap
} from './lib/utils';

import {
    findKey, omit, arraysEqual, mapValues
} from './lib/tools';

import type {
    IStickState, IButtonState, IButtonIndexes,
    IStickIndexes, IStickInverts, IListenOptions,
    IListenParams, IPlayerState, IPlayer
} from './types';

export default function createPlayer({
    name,
    threshold = 0.3,
    clampThreshold = true
}: { name: string, threshold: number, clampThreshold: boolean } = {}): IPlayer {
    let listenOptions: null | IListenOptions = null;

    const state: IPlayerState = {
        name,
        pad: {
            buttons: [],
            axes: []
        },
        prevPad: {
            buttons: [],
            axes: []
        },
        mappers: {},
        gamepadId: null,
        connected: false,
        buttons: getDefaultButtons(),
        sticks: getDefaultSticks()
    };

    const player: IPlayer = {
        getName: () => state.name,
        getGamepadId: () => state.gamepadId,
        isConnected: () => state.connected,

        getParsedGamepad: () => state.pad,

        clearMappers() {
            state.mappers = {};
        },

        removeMapper(mapperName: string) {
            state.mappers = omit([mapperName], state.mappers);
        },

        update(gamepad: Gamepad) {
            state.prevPad = state.pad;
            state.pad = parseGamepad(gamepad, state.prevPad, threshold, clampThreshold);

            listenOptions = updateListenOptions(listenOptions, state.pad, threshold);
        },

        getButtons(...inputNames: string[]): IButtonState | { [index: string]: IButtonState } {
            if (inputNames.length === 0) {
                return mapValues(button => buttonMap(state.pad, state.prevPad, button), state.buttons);
            }

            if (inputNames.length === 1) {
                return buttonMap(state.pad, state.prevPad, state.buttons[inputNames[0]]);
            }

            const result = {};
            inputNames.forEach(inputName => {
                result[inputName] = buttonMap(state.pad, state.prevPad, state.buttons[inputName]);
            });

            return result;
        },

        getSticks(...inputNames: string[]): IStickState | { [index: string]: IStickState } {
            if (inputNames.length === 0) {
                return mapValues(stick => {
                    const { indexes, inverts } = stick;
                    return stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
                }, state.sticks);
            }

            if (inputNames.length === 0) {
                const { indexes, inverts } = state.sticks[inputNames[0]];
                return stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
            }

            const result = {};
            inputNames.forEach(inputName => {
                const { indexes, inverts } = state.sticks[inputName];
                result[inputName] = stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
            });

            return result;
        },

        getMappers(...mapperNames: string[]): any | { [index: string]: any } {
            if (mapperNames.length === 0) {
                return mapValues(mapper => mapper({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                }), state.mappers);
            }

            if (mapperNames.length === 1) {
                return state.mappers[mapperNames[0]]({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                });
            }

            const result = {};
            mapperNames.forEach(mapperName => {
                result[mapperName] = state.mappers[mapperName]({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                });
            });

            return result;
        },

        getButtonIndexes(...inputNames: string[]): IButtonIndexes {
            const indexes = [];
            inputNames.forEach(inputName => indexes.push(...state.buttons[inputName]));
            return indexes;
        },

        getStickIndexes(...inputNames: string[]): IStickIndexes {
            const indexes = [];
            inputNames.forEach(inputName => indexes.push(...state.sticks[inputName].indexes));
            return indexes;
        },

        setButton(inputName: string, indexes: number | IButtonIndexes) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On setButton('${inputName}'): argument contains invalid characters`);
            }
            state.buttons[inputName] = (typeof indexes === 'number') ? [indexes] : indexes;
        },

        setStick(
            inputName: string,
            // #FlowExpectError No idea why this fails spectacularly, tried everything
            indexes: number[] | IStickIndexes,
            inverts?: IStickInverts
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On setStick('${inputName}'): argument contains invalid characters`);
            }

            if (indexes.length === 0) {
                throw new Error(`On setStick('${inputName}', indexes):
                    argument indexes is an empty array`);
            }

            const firstValue: number | number[] = indexes[0];

            if (Array.isArray(firstValue)) {
                state.sticks[inputName] = {
                    indexes,
                    inverts: inverts || firstValue.map(() => false)
                };
            } else {
                state.sticks[inputName] = {
                    indexes: [indexes],
                    inverts: inverts || indexes.map(() => false)
                };
            }
        },

        setMapper(mapperName: string, callback: Function) {
            if (!nameIsValid(mapperName)) {
                throw new Error(`On setMapper('${mapperName}', ...):
                    first argument contains invalid characters`);
            }

            state.mappers[mapperName] = callback;
        },

        invertSticks(inverts: IStickInverts, ...inputNames: string[]) {
            if (inputNames.length > 0) {
                inputNames.forEach(inputName => {
                    const stick = state.sticks[inputName];
                    if (stick.inverts.length === inverts.length) {
                        stick.inverts = inverts;
                    } else {
                        throw new Error(`On invertStick(inverts, [..., ${inputName}, ...]):
                            given argument inverts' length does not match '${inputName}' axis' length`);
                    }
                });
            }
        },

        swapButtons(btn1: string, btn2: string) {
            const { buttons } = state;
            // For some reason I can't do:
            // [buttons[btn1], buttons[btn2]] = [buttons[btn2], buttons[btn1]];
            const replacement = buttons[btn1];
            buttons[btn1] = buttons[btn2];
            buttons[btn2] = replacement;
        },

        swapSticks(btn1: string, btn2: string, includeInverts?: boolean = false) {
            const { sticks } = state;
            if (includeInverts) {
                const replacement = sticks[btn1];
                sticks[btn1] = sticks[btn2];
                sticks[btn2] = replacement;
            } else {
                const replacement = sticks[btn1].indexes;
                sticks[btn1].indexes = sticks[btn2].indexes;
                sticks[btn2].indexes = replacement;
            }
        },

        disconnect() {
            state.connected = false;
        },

        reconnect() {
            state.connected = true;
        },

        connect(gamepadId: string) {
            state.connected = true;
            state.gamepadId = gamepadId;
        },

        cancelListen() {
            listenOptions = null;
        },

        listenButton(
            callback: (indexes: number[]) => void,
            quantity?: number = 1,
            { waitFor = [1, 'polls'], consecutive = false, allowOffset = true }: IListenParams = {}
        ) {
            listenOptions = {
                callback,
                quantity,
                type: 'buttons',
                currentValue: 0,
                useTimeStamp: waitFor[1] === 'ms',
                targetValue: waitFor[0],
                consecutive,
                allowOffset
            };
        },

        listenAxis(
            callback: (indexes: number[]) => void,
            quantity: number = 2,
            { waitFor = [100, 'ms'], consecutive = true, allowOffset = true }: IListenParams = {}
        ) {
            listenOptions = {
                callback,
                quantity,
                type: 'axes',
                currentValue: 0,
                useTimeStamp: waitFor[1] === 'ms',
                targetValue: waitFor[0],
                consecutive,
                allowOffset
            };
        },

        buttonBindOnPress(
            inputName: string,
            callback: () => void,
            allowDuplication?: boolean = false
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On buttonBindOnPress('${inputName}', ...):
                    first argument contains invalid characters`);
            }
            player.listenButton(indexes => {
                const index = indexes[0];
                const bindingIndex = findKey(index, state.buttons);

                if (!allowDuplication && bindingIndex && state.buttons[inputName]) {
                    player.swapButtons(inputName, bindingIndex);
                } else {
                    player.setButton(inputName, index);
                }

                callback(bindingIndex);
            });
        },

        stickBindOnPress(
            inputName: string,
            callback: () => void,
            allowDuplication?: boolean = false
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On stickBindOnPress('${inputName}', ...):
                    first argument contains invalid characters`);
            }

            player.listenAxis((indexesResult: IStickIndexes) => {
                const c: Function = ({ indexes }) => arraysEqual(indexes, indexesResult);
                const bindingIndex: string | null = findKey(c, state.sticks);

                if (!allowDuplication && bindingIndex && state.sticks[inputName]) {
                    player.swapSticks(inputName, bindingIndex);
                } else {
                    player.setStick(inputName, indexesResult);
                }

                callback(bindingIndex);
            });
        },

        destroy() {
            player.disconnect();
            state.pad = {
                buttons: [],
                axes: []
            };
            state.prevPad = {
                buttons: [],
                axes: []
            };
            player.clearMappers();
        }
    };

    return player;
}
<commit_msg>Memoization TODO<commit_after>/* @flow */
import {
    updateListenOptions, nameIsValid,
    getDefaultButtons, getDefaultSticks, parseGamepad,
    stickMap, buttonMap
} from './lib/utils';

import {
    findKey, omit, arraysEqual, mapValues
} from './lib/tools';

import type {
    IStickState, IButtonState, IButtonIndexes,
    IStickIndexes, IStickInverts, IListenOptions,
    IListenParams, IPlayerState, IPlayer
} from './types';

export default function createPlayer({
    name,
    threshold = 0.3,
    clampThreshold = true
}: { name: string, threshold: number, clampThreshold: boolean } = {}): IPlayer {
    let listenOptions: null | IListenOptions = null;

    const state: IPlayerState = {
        name,
        pad: {
            buttons: [],
            axes: []
        },
        prevPad: {
            buttons: [],
            axes: []
        },
        mappers: {},
        gamepadId: null,
        connected: false,
        buttons: getDefaultButtons(),
        sticks: getDefaultSticks()
    };

    const player: IPlayer = {
        getName: () => state.name,
        getGamepadId: () => state.gamepadId,
        isConnected: () => state.connected,

        getParsedGamepad: () => state.pad,

        clearMappers() {
            state.mappers = {};
        },

        removeMapper(mapperName: string) {
            state.mappers = omit([mapperName], state.mappers);
        },

        update(gamepad: Gamepad) {
            // TODO Store the buttons, sticks and mappers that have already been queried
            // and clear those structures at the start of update
            // Maybe we can use those structures to avoid some costly calculations to stickMap's prevPressed
            // Maybe add a memoizeMappers flag (default true) to Player construction
            state.prevPad = state.pad;
            state.pad = parseGamepad(gamepad, state.prevPad, threshold, clampThreshold);

            listenOptions = updateListenOptions(listenOptions, state.pad, threshold);
        },

        getButtons(...inputNames: string[]): IButtonState | { [index: string]: IButtonState } {
            if (inputNames.length === 0) {
                return mapValues(button => buttonMap(state.pad, state.prevPad, button), state.buttons);
            }

            if (inputNames.length === 1) {
                return buttonMap(state.pad, state.prevPad, state.buttons[inputNames[0]]);
            }

            const result = {};
            inputNames.forEach(inputName => {
                result[inputName] = buttonMap(state.pad, state.prevPad, state.buttons[inputName]);
            });

            return result;
        },

        getSticks(...inputNames: string[]): IStickState | { [index: string]: IStickState } {
            if (inputNames.length === 0) {
                return mapValues(stick => {
                    const { indexes, inverts } = stick;
                    return stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
                }, state.sticks);
            }

            if (inputNames.length === 0) {
                const { indexes, inverts } = state.sticks[inputNames[0]];
                return stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
            }

            const result = {};
            inputNames.forEach(inputName => {
                const { indexes, inverts } = state.sticks[inputName];
                result[inputName] = stickMap(state.pad, state.prevPad, indexes, inverts, threshold);
            });

            return result;
        },

        getMappers(...mapperNames: string[]): any | { [index: string]: any } {
            if (mapperNames.length === 0) {
                return mapValues(mapper => mapper({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                }), state.mappers);
            }

            if (mapperNames.length === 1) {
                return state.mappers[mapperNames[0]]({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                });
            }

            const result = {};
            mapperNames.forEach(mapperName => {
                result[mapperName] = state.mappers[mapperName]({
                    pad: state.pad,
                    prevPad: state.prevPad,
                    player
                });
            });

            return result;
        },

        getButtonIndexes(...inputNames: string[]): IButtonIndexes {
            const indexes = [];
            inputNames.forEach(inputName => indexes.push(...state.buttons[inputName]));
            return indexes;
        },

        getStickIndexes(...inputNames: string[]): IStickIndexes {
            const indexes = [];
            inputNames.forEach(inputName => indexes.push(...state.sticks[inputName].indexes));
            return indexes;
        },

        setButton(inputName: string, indexes: number | IButtonIndexes) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On setButton('${inputName}'): argument contains invalid characters`);
            }
            state.buttons[inputName] = (typeof indexes === 'number') ? [indexes] : indexes;
        },

        setStick(
            inputName: string,
            // #FlowExpectError No idea why this fails spectacularly, tried everything
            indexes: number[] | IStickIndexes,
            inverts?: IStickInverts
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On setStick('${inputName}'): argument contains invalid characters`);
            }

            if (indexes.length === 0) {
                throw new Error(`On setStick('${inputName}', indexes):
                    argument indexes is an empty array`);
            }

            const firstValue: number | number[] = indexes[0];

            if (Array.isArray(firstValue)) {
                state.sticks[inputName] = {
                    indexes,
                    inverts: inverts || firstValue.map(() => false)
                };
            } else {
                state.sticks[inputName] = {
                    indexes: [indexes],
                    inverts: inverts || indexes.map(() => false)
                };
            }
        },

        setMapper(mapperName: string, callback: Function) {
            if (!nameIsValid(mapperName)) {
                throw new Error(`On setMapper('${mapperName}', ...):
                    first argument contains invalid characters`);
            }

            state.mappers[mapperName] = callback;
        },

        invertSticks(inverts: IStickInverts, ...inputNames: string[]) {
            if (inputNames.length > 0) {
                inputNames.forEach(inputName => {
                    const stick = state.sticks[inputName];
                    if (stick.inverts.length === inverts.length) {
                        stick.inverts = inverts;
                    } else {
                        throw new Error(`On invertStick(inverts, [..., ${inputName}, ...]):
                            given argument inverts' length does not match '${inputName}' axis' length`);
                    }
                });
            }
        },

        swapButtons(btn1: string, btn2: string) {
            const { buttons } = state;
            // For some reason I can't do:
            // [buttons[btn1], buttons[btn2]] = [buttons[btn2], buttons[btn1]];
            const replacement = buttons[btn1];
            buttons[btn1] = buttons[btn2];
            buttons[btn2] = replacement;
        },

        swapSticks(btn1: string, btn2: string, includeInverts?: boolean = false) {
            const { sticks } = state;
            if (includeInverts) {
                const replacement = sticks[btn1];
                sticks[btn1] = sticks[btn2];
                sticks[btn2] = replacement;
            } else {
                const replacement = sticks[btn1].indexes;
                sticks[btn1].indexes = sticks[btn2].indexes;
                sticks[btn2].indexes = replacement;
            }
        },

        disconnect() {
            state.connected = false;
        },

        reconnect() {
            state.connected = true;
        },

        connect(gamepadId: string) {
            state.connected = true;
            state.gamepadId = gamepadId;
        },

        cancelListen() {
            listenOptions = null;
        },

        listenButton(
            callback: (indexes: number[]) => void,
            quantity?: number = 1,
            { waitFor = [1, 'polls'], consecutive = false, allowOffset = true }: IListenParams = {}
        ) {
            listenOptions = {
                callback,
                quantity,
                type: 'buttons',
                currentValue: 0,
                useTimeStamp: waitFor[1] === 'ms',
                targetValue: waitFor[0],
                consecutive,
                allowOffset
            };
        },

        listenAxis(
            callback: (indexes: number[]) => void,
            quantity: number = 2,
            { waitFor = [100, 'ms'], consecutive = true, allowOffset = true }: IListenParams = {}
        ) {
            listenOptions = {
                callback,
                quantity,
                type: 'axes',
                currentValue: 0,
                useTimeStamp: waitFor[1] === 'ms',
                targetValue: waitFor[0],
                consecutive,
                allowOffset
            };
        },

        buttonBindOnPress(
            inputName: string,
            callback: () => void,
            allowDuplication?: boolean = false
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On buttonBindOnPress('${inputName}', ...):
                    first argument contains invalid characters`);
            }
            player.listenButton(indexes => {
                const index = indexes[0];
                const bindingIndex = findKey(index, state.buttons);

                if (!allowDuplication && bindingIndex && state.buttons[inputName]) {
                    player.swapButtons(inputName, bindingIndex);
                } else {
                    player.setButton(inputName, index);
                }

                callback(bindingIndex);
            });
        },

        stickBindOnPress(
            inputName: string,
            callback: () => void,
            allowDuplication?: boolean = false
        ) {
            if (!nameIsValid(inputName)) {
                throw new Error(`On stickBindOnPress('${inputName}', ...):
                    first argument contains invalid characters`);
            }

            player.listenAxis((indexesResult: IStickIndexes) => {
                const c: Function = ({ indexes }) => arraysEqual(indexes, indexesResult);
                const bindingIndex: string | null = findKey(c, state.sticks);

                if (!allowDuplication && bindingIndex && state.sticks[inputName]) {
                    player.swapSticks(inputName, bindingIndex);
                } else {
                    player.setStick(inputName, indexesResult);
                }

                callback(bindingIndex);
            });
        },

        destroy() {
            player.disconnect();
            state.pad = {
                buttons: [],
                axes: []
            };
            state.prevPad = {
                buttons: [],
                axes: []
            };
            player.clearMappers();
        }
    };

    return player;
}
<|endoftext|>


<commit_before>/**
 * Created by pubudud on 7/23/17.
 */
/**
 * Created by pubudud on 3/18/17.
 */

import Redis from 'redis';
import {
    DEFAULT_INTERVAL,
    STRATEGIES,
    noop,
    fakeLogger as logger,
    dirtyCheckers,
    getStatsString,
    CALLBACK_ARG_POSITION
} from './constants';

/**
 *  Redis backed queue for congestion control with a trade-off for data loss
 */
class QueueP {

    /**
     * Constructor
     */
    constructor() {
        this._dataMap = {};
        this._keyQueue = {};
        this.dirtyCheckers = {};
        this._strategy = STRATEGIES.STATE;
        this.logger = logger;
        this.stats = {};
    }

    /**
     * Initialize QueueP with a custom strategy rather than In-Memory-Application-State
     * @param {string} [strategy] - Redis or In-memory
     * @param {object} logger - an actual instance of some logger to override the noop logger
     * @param {function} [callback] - optional callback
     * @returns {Promise} <-
     */
    init(strategy, logger, callback) {
        return this.returnPromiseOrCallback(new Promise((resolve, reject) => {
            // Set Strategy
            if (strategy !== STRATEGIES.STATE || strategy !== STRATEGIES.REDIS) {
                let err = new Error(`Invalid Strategy: ${strategy}`);
                return reject(err);
            }
            this._strategy = strategy;
            if (this._strategy === STRATEGIES.REDIS) {
                this._redis = Redis.createClient();
            }

            // Set Logger
            this.logger = logger;

            return resolve();
        }), callback);
    }

    /**
     * Initialize a queue for a given hSet
     *
     * @param {string} [hSet] - Key of the parent hset(deprecated)
     * @param {string} [id] - alias for hSet
     * @param {number} [interval = 1000] - Interval for executing the consumer function
     * @param {function} [dirtyChecker] - Function to evaluate whether overriding value is different from earlier
     * @param {function} consumer - Function which implements the consumer logic
     */
    initQueue({hSet, id, interval = DEFAULT_INTERVAL, dirtyChecker, consumer}) {

        // Assign hSet alias value back to hSet if specified
        if (id) {
            hSet = id;
        }

        // Make a naive dirty checker if a dirty checker is not specified
        if (!dirtyChecker) {
            dirtyChecker = dirtyCheckers.naive();
        }

        this.dirtyCheckers[hSet] = dirtyChecker;
        this.stats[hSet] = {
            interval,
            processedCount: 0,
            failureCount: 0
        };

        setInterval(() => {
            if (!this._keyQueue[hSet]) {
                return;
            }
            let key = this._keyQueue[hSet].shift();

            if (!hSet || !key) {
                return;
            }

            this._getEntry(hSet, key)
                .then((entry) => {
                    if (entry && entry.isDirty) {

                        if (consumer.length <= CALLBACK_ARG_POSITION) { // Promise handlers for signalling using promise approach
                            consumer(key, entry.data)
                                .then(this._markEntryAsDone.bind(this, hSet, key, entry))
                                .catch(this._handleEntryConsumptionFailure.bind(this, hSet));

                        } else { // length > CALLBACK_ARG_POSITION => Callback provided => Callback for signalling using callback approach
                            consumer(key, entry.data, (err) => {
                                if (err)
                                    return this._handleEntryConsumptionFailure(hSet, err);

                                this._markEntryAsDone(hSet, key, entry);
                            });
                        }
                    }
                })
                .catch((err) => {
                    this.logger.error(err);
                });

        }, interval);
    }

    /**
     * Handle if an error occurred while running the consumer for a specific entry
     * Currently only a stat is collected. Can have a fallback mechanism or a retry strategy
     *
     * @param {string} hSet <-
     * @param {object} err <-
     * @private
     */
    _handleEntryConsumptionFailure(hSet, err) {
        noop(err);
        this.stats[hSet].failureCount++;
    }

    /**
     * Mark an entry as done processing. i.e -> set as not dirty(already processed)
     * @param {string} hSet <- parent set
     * @param {string} key <- key of the parent set (map structure)
     * @param {object} entry <-
     * @private
     */
    _markEntryAsDone(hSet, key, entry) {
        entry.isDirty = false;

        this._setEntry(hSet, key, entry)
            .then(noop) // Iteration success. Do nothing
            .catch((err) => {
                this.logger.error(err);
            });

        // Collect stat to print on demand
        this.stats[hSet].processedCount++;
    }

    /**
     * Fetch the entry with the target data from the data map(Redis or In-memory)
     *
     * @param {string} hSet - Key of the parent set
     * @param {string} key - Key of the actual target data
     * @private
     * @returns {Promise} <-
     */
    _getEntry(hSet, key) {
        return new Promise((resolve, reject) => {
            if (this._strategy === STRATEGIES.REDIS) {
                this._redis.hget(hSet, key, (err, res) => {
                    if (err) {
                        return reject(err);
                    }
                    if (!res) {
                        return resolve({});
                    }
                    return resolve(JSON.parse(res));
                });
            } else {
                if (!this._dataMap[hSet]) {
                    return resolve({});
                }
                return resolve(this._dataMap[hSet][key] || {});
            }
        });
    }

    /**
     * Add an entry to the data-map
     *
     * @param {string} hSet - Key of the parent set
     * @param {string} key - key of the target data
     * @param {Object} entry - Object consisting of the target data and the isDirty attribute
     * @private
     * @returns {Promise} <-
     */
    _setEntry(hSet, key, entry) {
        return new Promise((resolve, reject) => {
            if (this._strategy === STRATEGIES.REDIS) {

                this._redis.hset(hSet, key, JSON.stringify(entry), (err, res) => {
                    if (err) {
                        return reject(err);
                    }
                    return resolve(res);
                });

            } else {
                if (!this._dataMap[hSet]) {
                    this._dataMap[hSet] = {};
                }
                this._dataMap[hSet][key] = entry;
                return resolve();
            }
        });
    }

    /**
     * Update/Insert an entry of/to the dataset and enqueue to be delayed processed if the entry is evaluated as dirty
     *
     * @param {string} hSet - Id of the parent queue
     * @param {string} key - Key of the data to be queued
     * @param {Object|string|number} data - data to be enqueued for delayed processing
     * @param {function} [callback] - optional callback
     * @returns {Promise|undefined} <-
     */
    publish(hSet, key, data, callback) {
        return this.returnPromiseOrCallback(new Promise((resolve, reject) => {
            if (!this.dirtyCheckers[hSet]) {
                let err = new Error(`DirtyChecker not available for hSet: ${hSet}`);

                return reject(err);
            }
            this._getEntry(hSet, key)
                .then((entry) => {

                    let oldData = entry.data,
                        isDirty = this.dirtyCheckers[hSet](oldData, data);

                    if (!isDirty) {
                        return resolve();
                    }

                    // Set Entry and Enqueue if dirty
                    this
                        ._setEntry(hSet, key, {
                            data: data,
                            isDirty: true
                        })
                        .then(() => {
                            this._enqueue(hSet, key);
                            return resolve();
                        })
                        .catch((err) => reject(err));
                })
                .catch((err) => reject(err));
        }), callback);
    }

    /**
     * Enqueue to pending operations, if not already pending
     *
     * @param {!string} hSet - Key of Parent set
     * @param {!string} key - Key of actual target data
     * @private
     */
    _enqueue(hSet, key) {
        if (!this._keyQueue[hSet]) {
            this._keyQueue[hSet] = [];
        }
        if (this._keyQueue[hSet].indexOf(key) === -1) {
            this._keyQueue[hSet].push(key);
        }
    }

    /**
     * @param {string} hSet - target hset
     * @return {Object} - stat object
     */
    getStats(hSet) {
        return {
            queueLength: this._keyQueue[hSet].length,
            interval: this.stats[hSet].interval,
            totalProcessedCount: this.stats[hSet].processedCount,
            failureCount: this.stats[hSet].failureCount
        };
    }

    /**
     * @param {string} hSet - target hset
     * @param {Object} [logger] - override logger
     * @param {String} [level] - override log level
     */
    printStats(hSet, logger, level = 'info') {
        (logger || this.logger)[level](
            getStatsString(hSet, this._keyQueue[hSet] || [], this.stats[hSet])
        );
    }

    /**
     * Call callback function if present or return a promise to be handled by the caller
     * @param {Promise} promise <-
     * @param {Function} [callback] <-
     * @return {Promise|undefined} <-
     */
    returnPromiseOrCallback(promise, callback) {
        if (callback) {
            promise
                .then((result) => callback(null, result))
                .catch((err) => callback(err));
        }
        return promise;
    }
}

export default new QueueP();

export const strategies = STRATEGIES;

export const makeObjectDirtyChecker = dirtyCheckers.fieldBased;
<commit_msg>Use qInstance for providing a reference to an internal queue<commit_after>/**
 * Created by pubudud on 7/23/17.
 */
/**
 * Created by pubudud on 3/18/17.
 */

import Redis from 'redis';
import {
    DEFAULT_INTERVAL,
    STRATEGIES,
    noop,
    fakeLogger as logger,
    dirtyCheckers,
    getStatsString,
    CALLBACK_ARG_POSITION
} from './constants';
import QInstance from './qInstance';

/**
 *  Redis backed queue for congestion control with a trade-off for data loss
 */
class QueueP {

    /**
     * Constructor
     */
    constructor() {
        this._dataMap = {};
        this._keyQueue = {};
        this.dirtyCheckers = {};
        this._strategy = STRATEGIES.STATE;
        this.logger = logger;
        this.stats = {};
    }

    /**
     * Initialize QueueP with a custom strategy rather than In-Memory-Application-State
     * @param {string} [strategy] - Redis or In-memory
     * @param {object} logger - an actual instance of some logger to override the noop logger
     * @param {function} [callback] - optional callback
     * @returns {Promise} <-
     */
    init(strategy, logger, callback) {
        return this._returnPromiseOrCallback(new Promise((resolve, reject) => {
            // Set Strategy
            if (strategy !== STRATEGIES.STATE || strategy !== STRATEGIES.REDIS) {
                let err = new Error(`Invalid Strategy: ${strategy}`);
                return reject(err);
            }
            this._strategy = strategy;
            if (this._strategy === STRATEGIES.REDIS) {
                this._redis = Redis.createClient();
            }

            // Set Logger
            this.logger = logger;

            return resolve();
        }), callback);
    }

    /**
     * Initialize a queue for a given qId
     *
     * @param {!string} qId - a unique Id to identify the initialized queue from anywhere
     * @param {number} [interval = 1000] - Interval for executing the consumer function
     * @param {function} [dirtyChecker] - Function to evaluate whether overriding value is different from earlier
     * @param {function} consumer - Function which implements the consumer logic
     *
     * @returns {object} - instance of QInstance to allow easy interaction with the initialized queue
     */
    initQueue(qId, {interval = DEFAULT_INTERVAL, dirtyChecker, consumer}) {

        // Make a naive dirty checker if a dirty checker is not specified
        if (!dirtyChecker) {
            dirtyChecker = dirtyCheckers.naive();
        }

        this.dirtyCheckers[qId] = dirtyChecker;
        this.stats[qId] = {
            interval,
            processedCount: 0,
            failureCount: 0
        };

        setInterval(() => {
            if (!this._keyQueue[qId]) {
                return;
            }
            let key = this._keyQueue[qId].shift();

            if (!qId || !key) {
                return;
            }

            this._getEntry(qId, key)
                .then((entry) => {
                    if (entry && entry.isDirty) {

                        if (consumer.length <= CALLBACK_ARG_POSITION) { // Promise handlers for signalling using promise approach
                            consumer(key, entry.data)
                                .then(this._markEntryAsDone.bind(this, qId, key, entry))
                                .catch(this._handleEntryConsumptionFailure.bind(this, qId));

                        } else { // length > CALLBACK_ARG_POSITION => Callback provided => Callback for signalling using callback approach
                            consumer(key, entry.data, (err) => {
                                if (err)
                                    return this._handleEntryConsumptionFailure(qId, err);

                                this._markEntryAsDone(qId, key, entry);
                            });
                        }
                    }
                })
                .catch((err) => {
                    this.logger.error(err);
                });

        }, interval);

        return new QInstance(qId);
    }

    /**
     * @param {string} qId - Id of an initialized Queue to create and return a reference
     * @return {QInstance} - Instance of a queue
     */
    getQueueInstance(qId) {
        if (!this._keyQueue[qId] || !this.stats[qId] || !this.dirtyCheckers[qId]) {
            throw new Error("Broken or incomplete Queue!");
        }
        return new QInstance(qId);
    }

    /**
     * Handle if an error occurred while running the consumer for a specific entry
     * Currently only a stat is collected. Can have a fallback mechanism or a retry strategy
     *
     * @param {string} qId <-
     * @param {object} err <-
     * @private
     */
    _handleEntryConsumptionFailure(qId, err) {
        noop(err);
        this.stats[qId].failureCount++;
    }

    /**
     * Mark an entry as done processing. i.e -> set as not dirty(already processed)
     * @param {string} qId <- parent set
     * @param {string} key <- key of the parent set (map structure)
     * @param {object} entry <-
     * @private
     */
    _markEntryAsDone(qId, key, entry) {
        entry.isDirty = false;

        this._setEntry(qId, key, entry)
            .then(noop) // Iteration success. Do nothing
            .catch((err) => {
                this.logger.error(err);
            });

        // Collect stat to print on demand
        this.stats[qId].processedCount++;
    }

    /**
     * Fetch the entry with the target data from the data map(Redis or In-memory)
     *
     * @param {string} qId - Key of the parent set
     * @param {string} key - Key of the actual target data
     * @private
     * @returns {Promise} <-
     */
    _getEntry(qId, key) {
        return new Promise((resolve, reject) => {
            if (this._strategy === STRATEGIES.REDIS) {
                this._redis.hget(qId, key, (err, res) => {
                    if (err) {
                        return reject(err);
                    }
                    if (!res) {
                        return resolve({});
                    }
                    return resolve(JSON.parse(res));
                });
            } else {
                if (!this._dataMap[qId]) {
                    return resolve({});
                }
                return resolve(this._dataMap[qId][key] || {});
            }
        });
    }

    /**
     * Add an entry to the data-map
     *
     * @param {string} qId - Key of the parent set
     * @param {string} key - key of the target data
     * @param {Object} entry - Object consisting of the target data and the isDirty attribute
     * @private
     * @returns {Promise} <-
     */
    _setEntry(qId, key, entry) {
        return new Promise((resolve, reject) => {
            if (this._strategy === STRATEGIES.REDIS) {

                this._redis.hset(qId, key, JSON.stringify(entry), (err, res) => {
                    if (err) {
                        return reject(err);
                    }
                    return resolve(res);
                });

            } else {
                if (!this._dataMap[qId]) {
                    this._dataMap[qId] = {};
                }
                this._dataMap[qId][key] = entry;
                return resolve();
            }
        });
    }

    /**
     * Update/Insert an entry of/to the dataset and enqueue to be delayed processed if the entry is evaluated as dirty
     *
     * @param {string} qId - Id of the parent queue
     * @param {string} key - Key of the data to be queued
     * @param {Object|string|number} data - data to be enqueued for delayed processing
     * @param {function} [callback] - optional callback
     * @returns {Promise|undefined} <-
     */
    publish(qId, key, data, callback) {
        return this._returnPromiseOrCallback(new Promise((resolve, reject) => {
            if (!this.dirtyCheckers[qId]) {
                let err = new Error(`DirtyChecker unavailable/destroyed for qId: ${qId}`);

                return reject(err);
            }
            this._getEntry(qId, key)
                .then((entry) => {

                    let oldData = entry.data,
                        isDirty = this.dirtyCheckers[qId](oldData, data);

                    if (!isDirty) {
                        return resolve();
                    }

                    // Set Entry and Enqueue if dirty
                    this
                        ._setEntry(qId, key, {
                            data: data,
                            isDirty: true
                        })
                        .then(() => {
                            this._enqueue(qId, key);
                            return resolve();
                        })
                        .catch(reject);
                })
                .catch(reject);
        }), callback);
    }

    /**
     * Enqueue to pending operations, if not already pending
     *
     * @param {!string} qId - Key of Parent set
     * @param {!string} key - Key of actual target data
     * @private
     */
    _enqueue(qId, key) {
        if (!this._keyQueue[qId]) {
            this._keyQueue[qId] = [];
        }
        if (this._keyQueue[qId].indexOf(key) === -1) {
            this._keyQueue[qId].push(key);
        }
    }

    /**
     * @param {string} qId - target queue
     * @return {Object} - stat object
     * @param {function} [callback] - optional callback
     *
     * @return {Promise|undefined} <-
     */
    getStats(qId, callback) {
        return this._returnPromiseOrCallback(new Promise((resolve, reject)=> {
            if (!this._keyQueue[qId] || !this.stats[qId]) {
                return reject(new Error("Broken or incomplete Queue!"));
            }
            return resolve({
                queueLength: this._keyQueue[qId].length,
                interval: this.stats[qId].interval,
                totalProcessedCount: this.stats[qId].processedCount,
                failureCount: this.stats[qId].failureCount
            });
        }), callback);
    }

    /**
     * @param {string} qId - target queue
     * @param {Object} [logger] - override logger
     * @param {String} [level] - override log level
     */
    printStats(qId, logger, level = 'info') {
        if (!this._keyQueue[qId] || !this.stats[qId]) {
            (logger || this.logger).error("Broken or incomplete Queue!");
            return;
        }
        (logger || this.logger)[level](
            getStatsString(qId, this._keyQueue[qId] || [], this.stats[qId])
        );
    }

    /**
     * Call callback function if present or return a promise to be handled by the caller
     * @param {Promise} promise <-
     * @param {Function} [callback] <-
     * @return {Promise|undefined} <-
     * @private
     */
    _returnPromiseOrCallback(promise, callback) {
        if (callback) {
            promise
                .then((result) => callback(null, result))
                .catch((err) => callback(err));
        }
        return promise;
    }
}

export default new QueueP();

export const strategies = STRATEGIES;

export const makeObjectDirtyChecker = dirtyCheckers.fieldBased;
<|endoftext|>