<commit_before>angular
    .module('app.home')
    .controller('HomeController', HomeController);

// HomeController.$inject = ['pageTrials', 'allTrials', 'trialService', '$timeout', 'isEmptyFilter', 'strReplaceFilter'];
HomeController.$inject = ['pageTrials', 'trialService', '$timeout', 'isEmptyFilter', 'strReplaceFilter'];

// function HomeController(pageTrials, allTrials, trialService, $timeout, isEmptyFilter, strReplaceFilter) {
function HomeController(pageTrials, trialService, $timeout, isEmptyFilter, strReplaceFilter) {
    var vm = this;
    vm.results = [];
    vm.searched = false;
    vm.filterData = 0;
    vm.selectOptions = false;
    vm.searching = false;
    vm.searchBtnClicked = false;
    vm.trialSelected = false;
    vm.disableInputField = true;
    vm.selected = {};
    vm.sortColumn = 'plot_id';
    vm.reverse = false;
    vm.replaceValue = {Plus: true, Minus: false};
    vm.maxSize = 5;
    vm.totalResults = 0;
    vm.currentPage = 1;
    vm.pageSize = 5; // Maximum page size
    vm.pageParams = {offset: 0, limit: vm.pageSize};
    vm.baseURL = "trials/treatment/";
    vm.options = {psize: [5, 10, 25, 50]};
    vm.filterSelectOptions = ['trial_id', 'observation', 'year', 'season', 'tillage_practice', 'farm_yard_manure', 'farm_residue', 'nitrogen_treatment', 'phosphate_treatment'];
    vm.filterTableData = ['trial_id', 'plot_id', 'sub_plot_id', '', 'observation', 'year', 'tillage_practice', 'farm_yard_manure', 'farm_residue', 'crops_grown', 'nitrogen_treatment', 'phosphate_treatment', 'short_rains', 'long_rains'];

        // Get table trials data
    vm.getTrials = function(data) {
        vm.trialsData = [];
        angular.forEach(data, function (obj) {
            var objLevel = '', firstLevelData = '', secondLevelData = '',  thirdLevelData = '';
            angular.forEach(obj, function(value){
                if (value !== undefined && value !== null && typeof value === 'object'){
                    objLevel = trialService.getNestedTrials(value);
                    secondLevelData = trialService.filterMultiObj(objLevel.outObjArr, vm.filterTableData);
                    if (objLevel.nestedObj){
                        objLevel = trialService.getNestedTrials(objLevel.nestedObj, 'trial_yield', ['Short Rains', 'Long Rains']);
                        thirdLevelData = trialService.filterMultiObj(objLevel.outObjArr, vm.filterTableData);
                        thirdLevelData = trialService.mergeOnSearch(thirdLevelData, ['observation', 'year']);
                    }
                }else{
                    firstLevelData = trialService.filterMultiObj([obj], vm.filterTableData, vm.replaceValue);
                }
            });
            // Merge all level data starting with the third level repeating the succeeding levels
            angular.forEach(thirdLevelData, function(thirdObj){
                angular.forEach(secondLevelData, function(secondObj){
                    angular.forEach(firstLevelData, function(firstObj){
                        vm.trialsData = vm.trialsData.concat(angular.merge({}, firstObj, secondObj, thirdObj));
                    });
                });
            });
        });
        return vm.trialsData;
    };

    vm.setResults = function (response) {
        var outObj = [];
        var respResults = vm.getTrials(response.results);
        if (vm.searchBtnClicked) {
            angular.forEach(respResults, function (resultsObj){
                var objMatch = [];
                angular.forEach(vm.selected, function (selValue, selKey){
                    if (angular.isArray(selValue)){
                        var inObjMatch = [];
                        angular.forEach(selValue, function (subSelValue) {
                            if (subSelValue[selKey] === 'Short Rains' || subSelValue[selKey] === 'Long Rains'){
                                var newKey = strReplaceFilter(subSelValue[selKey], ' ', '_').toLowerCase();
                                resultsObj.hasOwnProperty(newKey) ? inObjMatch.push(true) : inObjMatch.push(false);
                            }else{
                                subSelValue[selKey] === resultsObj[selKey] ? inObjMatch.push(true) : inObjMatch.push(false);
                            }
                        });
                        (inObjMatch.indexOf(true) !== -1 || selValue.length === 0) ? objMatch.push(true) : objMatch.push(false);
                    }else if (typeof selValue === 'object'){
                        selValue[selKey] === resultsObj[selKey] ? objMatch.push(true) : objMatch.push(false);
                    }
                });
                if (objMatch.indexOf(false) === -1) outObj = outObj.concat(resultsObj);
            });
            vm.results = outObj;
        }else{
            vm.totalResults = response.count;
            vm.results = respResults;
            vm.filterData = vm.results.length;
        }
    };

    // vm.getTrialsSearchOptions = function () {
    //     vm.searching = false;
    //     vm.selectOptions = trialService.filterSingleObj(allTrials, vm.filterSelectOptions, vm.replaceValue);
    //     $timeout(function () {
    //         vm.searching = false;
    //     }, 500);
    // };
    // vm.getTrialsSearchOptions();

    vm.getPageTrials = function () {
        if (pageTrials !== undefined || pageTrials !== null) {
            vm.searching = false;
            vm.setResults(pageTrials);
            $timeout(function () {
                vm.searching = false;
            }, 500);
        }
    };
    vm.getPageTrials();

    // Search one or more records in all pages
    // Usage: Default params: vm.queryAllpages("trials/treatment/",{offset: 0, limit: 50})
    vm.queryPage = function (apiNode, query) {
        vm.searching = true;
        trialService.search(apiNode, query).then(function (response) {
            vm.setResults(response);
            $timeout(function () {
                vm.searching = false;
            }, 500);
        });
    };

    // /**
    //  * TODO: DO NOT DELETE this function is so important.
    //  * Search one or more records in all pages
    //  * Usage: Default params: vm.queryAllpages("trials/treatment/",{offset: 0, limit: 50})
    //  **/
    // vm.queryAllpages = function (apiNode, query) {
    //     vm.searching = true;
    //     trialService.searchAllPages(apiNode, query, []).then(function (response) {
    //         vm.selectOptions = trialService.filterSingleObj(response, vm.filterSelectOptions, vm.replaceValue);
    //         $timeout(function () {
    //             vm.searching = false;
    //         }, 500);
    //     });
    // };

    vm.searchTrials = function (){
        if (!isEmptyFilter(vm.selected)) {
            vm.searchBtnClicked = true;
            vm.queryPage(vm.baseURL, vm.pageParams);
        }else{
            vm.searchBtnClicked = false;
        }

    };

    vm.sort_with = function(column) {
        vm.reverse = (vm.sortColumn === column) ? !vm.reverse : false;
        vm.sortColumn = column;
    };

    vm.onSearchTable = function() {
        $timeout(function() {
            vm.filterData = vm.searched.length;
        }, 20);
    };

    vm.changePageSize = function () {
        vm.pageParams.limit = vm.pageSize;
        vm.queryPage(vm.baseURL, vm.pageParams);
    };

    vm.pageChanged = function () {
        vm.pageParams.offset = (vm.currentPage - 1) * vm.pageSize;
        vm.queryPage(vm.baseURL, vm.pageParams);
    };
}
<commit_msg>changed variables<commit_after>angular
    .module('app.home')
    .controller('HomeController', HomeController);

// HomeController.$inject = ['pageTrials', 'allTrials', 'trialService', '$timeout', 'isEmptyFilter', 'strReplaceFilter'];
HomeController.$inject = ['pageTrials', 'trialService', '$timeout', 'isEmptyFilter', 'strReplaceFilter'];

// function HomeController(pageTrials, allTrials, trialService, $timeout, isEmptyFilter, strReplaceFilter) {
function HomeController(pageTrials, trialService, $timeout, isEmptyFilter, strReplaceFilter) {
    var vm = this;
    vm.results = [];
    vm.searched = false;
    vm.filterData = 0;
    vm.selectOptions = false;
    vm.searching = false;
    vm.searchBtnClicked = false;
    vm.trialSelected = false;
    vm.disableInputField = true;
    vm.selected = {};
    vm.sortColumn = 'plot_id';
    vm.reverse = false;
    vm.replaceValue = {Plus: true, Minus: false};
    vm.maxSize = 5;
    vm.totalResults = 0;
    vm.currentPage = 1;
    vm.pageSize = 5; // Maximum page size
    vm.pageParams = {offset: 0, limit: vm.pageSize};
    vm.baseURL = "trials/treatment/";
    vm.options = {psize: [5, 10, 25, 50]};
    vm.filterSelectOptions = ['trial_id', 'observation', 'year', 'season', 'tillage_practice', 'farm_yard_manure', 'farm_residue', 'nitrogen_treatment', 'phosphate_treatment'];
    vm.filterTableData = ['trial_id', 'plot_id', 'sub_plot_id', '', 'observation', 'year', 'tillage_practice', 'farm_yard_manure', 'farm_residue', 'crops_grown', 'nitrogen_treatment', 'phosphate_treatment', 'short_rains', 'long_rains'];

        // Get table trials data
    vm.getTrials = function(data) {
        vm.trialsData = [];
        angular.forEach(data, function (obj) {
            var objLevel = '', firstLevelData = '', secondLevelData = '',  thirdLevelData = '';
            angular.forEach(obj, function(value){
                if (value !== undefined && value !== null && typeof value === 'object'){
                    objLevel = trialService.getNestedTrials(value);
                    secondLevelData = trialService.filterMultiObj(objLevel.outObjArr, vm.filterTableData);
                    if (objLevel.nestedObj){
                        objLevel = trialService.getNestedTrials(objLevel.nestedObj, 'trial_yield', ['Short Rains', 'Long Rains']);
                        thirdLevelData = trialService.filterMultiObj(objLevel.outObjArr, vm.filterTableData);
                        thirdLevelData = trialService.mergeOnSearch(thirdLevelData, ['observation', 'year']);
                    }
                }else{
                    firstLevelData = trialService.filterMultiObj([obj], vm.filterTableData, vm.replaceValue);
                }
            });
            // Merge all level data starting with the third level repeating the succeeding levels
            angular.forEach(thirdLevelData, function(thirdObj){
                angular.forEach(secondLevelData, function(secondObj){
                    angular.forEach(firstLevelData, function(firstObj){
                        vm.trialsData = vm.trialsData.concat(angular.merge({}, firstObj, secondObj, thirdObj));
                    });
                });
            });
        });
        return vm.trialsData;
    };

    vm.setResults = function (response) {
        var outObj = [];
        vm.totalResults = response.count;
        vm.results = vm.getTrials(response.results);
        if (vm.searchBtnClicked) {
            angular.forEach(vm.results, function (resultsObj){
                var objMatch = [];
                angular.forEach(vm.selected, function (selValue, selKey){
                    if (angular.isArray(selValue)){
                        var inObjMatch = [];
                        angular.forEach(selValue, function (subSelValue) {
                            if (subSelValue[selKey] === 'Short Rains' || subSelValue[selKey] === 'Long Rains'){
                                var newKey = strReplaceFilter(subSelValue[selKey], ' ', '_').toLowerCase();
                                resultsObj.hasOwnProperty(newKey) ? inObjMatch.push(true) : inObjMatch.push(false);
                            }else{
                                subSelValue[selKey] === resultsObj[selKey] ? inObjMatch.push(true) : inObjMatch.push(false);
                            }
                        });
                        (inObjMatch.indexOf(true) !== -1 || selValue.length === 0) ? objMatch.push(true) : objMatch.push(false);
                    }else if (typeof selValue === 'object'){
                        selValue[selKey] === resultsObj[selKey] ? objMatch.push(true) : objMatch.push(false);
                    }
                });
                if (objMatch.indexOf(false) === -1) outObj = outObj.concat(resultsObj);
            });
            vm.results = outObj;
        }
        vm.filterData = vm.results.length;
    };

    // vm.getTrialsSearchOptions = function () {
    //     vm.searching = false;
    //     vm.selectOptions = trialService.filterSingleObj(allTrials, vm.filterSelectOptions, vm.replaceValue);
    //     $timeout(function () {
    //         vm.searching = false;
    //     }, 500);
    // };
    // vm.getTrialsSearchOptions();

    vm.getPageTrials = function () {
        if (pageTrials !== undefined || pageTrials !== null) {
            vm.searching = false;
            vm.setResults(pageTrials);
            $timeout(function () {
                vm.searching = false;
            }, 500);
        }
    };
    vm.getPageTrials();

    // Search one or more records in all pages
    // Usage: Default params: vm.queryAllpages("trials/treatment/",{offset: 0, limit: 50})
    vm.queryPage = function (apiNode, query) {
        vm.searching = true;
        trialService.search(apiNode, query).then(function (response) {
            vm.setResults(response);
            $timeout(function () {
                vm.searching = false;
            }, 500);
        });
    };

    // /**
    //  * TODO: DO NOT DELETE this function is so important.
    //  * Search one or more records in all pages
    //  * Usage: Default params: vm.queryAllpages("trials/treatment/",{offset: 0, limit: 50})
    //  **/
    // vm.queryAllpages = function (apiNode, query) {
    //     vm.searching = true;
    //     trialService.searchAllPages(apiNode, query, []).then(function (response) {
    //         vm.selectOptions = trialService.filterSingleObj(response, vm.filterSelectOptions, vm.replaceValue);
    //         $timeout(function () {
    //             vm.searching = false;
    //         }, 500);
    //     });
    // };

    vm.searchTrials = function (){
        if (!isEmptyFilter(vm.selected)) {
            vm.searchBtnClicked = true;
            vm.queryPage(vm.baseURL, vm.pageParams);
        }else{
            vm.searchBtnClicked = false;
        }

    };

    vm.sort_with = function(column) {
        vm.reverse = (vm.sortColumn === column) ? !vm.reverse : false;
        vm.sortColumn = column;
    };

    vm.onSearchTable = function() {
        $timeout(function() {
            vm.filterData = vm.searched.length;
        }, 20);
    };

    vm.changePageSize = function () {
        vm.pageParams.limit = vm.pageSize;
        vm.queryPage(vm.baseURL, vm.pageParams);
    };

    vm.pageChanged = function () {
        vm.pageParams.offset = (vm.currentPage - 1) * vm.pageSize;
        vm.queryPage(vm.baseURL, vm.pageParams);
    };
}
<|endoftext|>


<commit_before>var _ = require('underscore');
var AnalysisPoller = require('./analysis/analysis-poller');
var AnonymousMapSerializer = require('./windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer');
var Backbone = require('backbone');
var CartoDBLayerGroup = require('./geo/cartodb-layer-group');
var DataviewsCollection = require('./dataviews/dataviews-collection');
var LayersCollection = require('./geo/map/layers');
var ModelUpdater = require('./windshaft-integration/model-updater');
var NamedMapSerializer = require('./windshaft/map-serializer/named-map-serializer/named-map-serializer');
var Request = require('./windshaft/request');
var Response = require('./windshaft/response');
var WindshaftClient = require('./windshaft/client');
var AnalysisService = require('./analysis/analysis-service');
var parseWindshaftErrors = require('./windshaft/error-parser');
var log = require('./cdb.log');
var WindshaftError = require('./windshaft/error');

/**
 *
 * Creates a new Engine.
 * An engine is the core of a carto app.
 *
 * With the help of external services the engine will:
 *
 *  - Keep the state of the layers and dataviews.
 *  - Serialize the state and send requests to the server.
 *  - Parse the server response and update the internal models.
 *  - Notify errors or successful operations.
 *
 * @param {Object} params - The parameters to initialize the engine.
 * @param {string} params.apiKey - Api key used to be autenticate in the windshaft server.
 * @param {string} params.authToken - Token used to be autenticate in the windshaft server.
 * @param {string} params.username - Name of the user registered in the windshaft server.
 * @param {string} params.serverUrl - Url of the windshaft server.
 * @param {boolean} params.templateName - While we dont remove named maps we must explicitly say when the map is named. Defaults to false.
 * @param {boolean} params.statTag - Token used to get map view statistics.
 * @class
 * @api
 */
function Engine (params) {
  if (!params) throw new Error('new Engine() called with no paramters');
  this._isNamedMap = params.templateName !== undefined;

  this._windshaftSettings = {
    urlTemplate: params.serverUrl,
    userName: params.username,
    statTag: params.statTag,
    apiKey: params.apiKey,
    authToken: params.authToken,
    templateName: params.templateName
  };

  this._windshaftClient = new WindshaftClient(this._windshaftSettings);

  // This object will be responsible of triggering the engine events.
  this._eventEmmitter = _.extend({}, Backbone.Events);

  this._analysisPoller = new AnalysisPoller();
  this._layersCollection = new LayersCollection();
  this._dataviewsCollection = new DataviewsCollection();

  this._cartoLayerGroup = new CartoDBLayerGroup(
    { apiKey: params.apiKey, authToken: params.authToken },
    { layersCollection: this._layersCollection }
  );

  this._modelUpdater = new ModelUpdater({
    dataviewsCollection: this._dataviewsCollection,
    layerGroupModel: this._cartoLayerGroup,
    layersCollection: this._layersCollection
  });
}

/**
 * Return the cartoLayergroup attached to the engine
 */
Engine.prototype.getLayerGroup = function () {
  return this._cartoLayerGroup;
};

/**
 * Bind a callback function to an event. The callback will be invoked whenever the event is fired.
 *
 * @param {string} event - The name of the event that triggers the callback execution.
 * @param {function} callback - A function to be executed when the event is fired.
 * @param {function} [context] - The context value for this when the callback is invoked.
 *
 * @example
 *
 * // Define a callback to be executed once the map is reloaded.
 * function onReload(event) {
 *  console.log(event); // "reload-success"
 * }
 *
 * // Attach the callback to the RELOAD_SUCCESS event.
 * engine.on(Engine.Events.RELOAD_SUCCESS, onReload);
 *
 * // Call the reload method and wait.
 * engine.reload();
 *
 * @api
 */
Engine.prototype.on = function (event, callback, context) {
  this._eventEmmitter.on(event, callback, context);
};

/**
 * Remove a previously-bound callback function from an event.
 *
 * @param {string} event - The name of the event that triggers the callback execution.
 * @param {function} callback - A function callback to be removed when the event is fired.
 * @param {function} [context] - The context value for this when the callback is invoked.
 *
 * @example
 *
 * // Remove the the `displayMap` listener function so it wont be executed anymore when the engine fires the `load` event.
 * engine.off(Engine.Events.RELOAD_SUCCESS, onReload);
 *
 * @api
 */
Engine.prototype.off = function (event, callback, context) {
  this._eventEmmitter.off(event, callback, context);
};

/**
 * This is the most important function of the engine.
 * Generate a payload from the current state, send it to the windshaft server
 * and update the internal models with the server response.
 *
 * Once the response has arrived trigger a 'reload-succes' or 'reload-error' event.
 *
 * @param {string} options.sourceId - The sourceId triggering the reload event. This is usefull to prevent uneeded requests and save data.
 * @param {boolean} options.forceFetch - Forces dataviews to fetch data from server after a reload
 * @param {boolean} options.includeFilters - Boolean flag to control if the filters need to be added in the payload.
 *
 * @fires Engine#Engine:RELOAD_STARTED
 * @fires Engine#Engine:RELOAD_SUCCESS
 * @fires Engine#Engine:RELOAD_ERROR
 *
 * @api
 */
Engine.prototype.reload = function (options) {
  options = this._buildOptions(options);
  try {
    var params = this._buildParams(options.includeFilters);
    var payload = this._getSerializer().serialize(this._layersCollection, this._dataviewsCollection);
    var request = new Request(payload, params, options);

    this._eventEmmitter.trigger(Engine.Events.RELOAD_STARTED);
    this._windshaftClient.instantiateMap(request);
  } catch (error) {
    this._manageClientError(error, options);
  }
};

/**
 *
 * Add a layer to the engine layersCollection
 *
 * @param {layer} layer - A new layer to be added to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.addLayer = function (layer) {
  this._layersCollection.add(layer);
};

/**
 *
 * Remove a layer from the engine layersCollection
 *
 * @param {layer} layer - A new layer to be removed from the engine.
 *
 * @public
 * @api
 */
Engine.prototype.removeLayer = function (layer) {
  this._layersCollection.remove(layer);
};

/**
 *
 * Add a dataview to the engine dataviewsCollection
 *
 * @param {Dataview} dataview - A new dataview to be added to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.addDataview = function (dataview) {
  this._dataviewsCollection.add(dataview);
};

/**
 *
 * Remove a dataview from the engine dataviewsCollection
 *
 * @param {Dataview} dataview - The Dataview to be removed to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.removeDataview = function (dataview) {
  this._dataviewsCollection.remove(dataview);
};

/**
 * Callback executed when the windhsaft client returns a successful response.
 * Update internal models and trigger a reload_sucess event.
 * @private
 */
Engine.prototype._onReloadSuccess = function (serverResponse, options) {
  var responseWrapper = new Response(this._windshaftSettings, serverResponse);
  this._modelUpdater.updateModels(responseWrapper, options.sourceId, options.forceFetch);
  this._restartAnalysisPolling();
  this._eventEmmitter.trigger(Engine.Events.RELOAD_SUCCESS);
  options.success && options.success();
};

/**
 * Callback executed when the windhsaft client returns a failed response.
 * Update internal models setting errores and trigger a reload_error event.
 * @private
 */
Engine.prototype._onReloadError = function (serverResponse, options) {
  var windshaftErrors = parseWindshaftErrors(serverResponse);
  var error = _.find(windshaftErrors, function (error) { return error.isGlobalError(); });
  this._modelUpdater.setErrors(windshaftErrors);
  this._eventEmmitter.trigger(Engine.Events.RELOAD_ERROR, windshaftErrors);
  options.error && options.error(error);
};

/**
 * Helper to get windhsaft request options.
 * @private
 */
Engine.prototype._buildOptions = function (options) {
  options = options || {};
  return _.extend({
    includeFilters: true,
    success: function (serverResponse) {
      this._onReloadSuccess(serverResponse, options);
    }.bind(this),
    error: function (serverResponse) {
      this._onReloadError(serverResponse, options);
    }.bind(this)
  }, _.pick(options, 'sourceId', 'forceFetch', 'includeFilters'));
};

/**
 * Helper to get windhsaft request parameters.
 * @param {boolean} includeFilters - Boolean flag to control if the filters need to be added in the payload.
 * @private
 */
Engine.prototype._buildParams = function (includeFilters) {
  var params = {
    stat_tag: this._windshaftSettings.statTag
  };
  if (includeFilters && !_.isEmpty(this._dataviewsCollection.getFilters())) {
    params.filters = this._dataviewsCollection.getFilters();
  }
  if (this._windshaftSettings.apiKey) {
    params.api_key = this._windshaftSettings.apiKey;
    return params;
  }
  if (this._windshaftSettings.authToken) {
    params.auth_token = this._windshaftSettings.authToken;
    return params;
  }
  console.error('Engine initialized with no apiKeys neither authToken');
};

/**
 * Reset the analysis nodes in the poller
 * @private
 */
Engine.prototype._restartAnalysisPolling = function () {
  var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection);
  this._analysisPoller.resetAnalysisNodes(analysisNodes);
};

/**
 * Get the instance of the serializer service depending on is an anonymous or a named map.
 * @private
 */
Engine.prototype._getSerializer = function () {
  return this._isNamedMap ? NamedMapSerializer : AnonymousMapSerializer;
};

/**
 * Manage and propagate the client error
 * @private
 */
Engine.prototype._manageClientError = function (error, options) {
  var windshaftError = new WindshaftError({
    message: error.message
  });
  this._modelUpdater.setErrors([windshaftError]);

  log.error(error.message);
  options.error && options.error();
};

/**
 * Events fired by the engine
 *
 * @readonly
 * @enum {string}
 * @api
 */
Engine.Events = {
  /**
   * Reload started event, fired every time the reload process starts.
   */
  RELOAD_STARTED: 'reload-started',
  /**
   * Reload success event, fired every time the reload function succeed.
   */
  RELOAD_SUCCESS: 'reload-success',
  /**
   * Reload success event, fired every time the reload function fails.
   */
  RELOAD_ERROR: 'reload-error'
};

module.exports = Engine;

/**
 * Reload started event, fired every time the reload process starts.
 *
 * @event Engine#Engine:RELOAD_STARTED
 * @type {string}
 * @api
 */

/**
  * Reload success event, fired every time the reload function succeed.
  *
  * @event Engine#Engine:RELOAD_SUCCESS
  * @type {string}
  * @api
  */

/**
  * Reload success event, fired every time the reload function fails.
  *
  * @event Engine#Engine:RELOAD_ERROR
  * @type {string}
  * @api
  */
<commit_msg>Fix error propagations<commit_after>var _ = require('underscore');
var AnalysisPoller = require('./analysis/analysis-poller');
var AnonymousMapSerializer = require('./windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer');
var Backbone = require('backbone');
var CartoDBLayerGroup = require('./geo/cartodb-layer-group');
var DataviewsCollection = require('./dataviews/dataviews-collection');
var LayersCollection = require('./geo/map/layers');
var ModelUpdater = require('./windshaft-integration/model-updater');
var NamedMapSerializer = require('./windshaft/map-serializer/named-map-serializer/named-map-serializer');
var Request = require('./windshaft/request');
var Response = require('./windshaft/response');
var WindshaftClient = require('./windshaft/client');
var AnalysisService = require('./analysis/analysis-service');
var parseWindshaftErrors = require('./windshaft/error-parser');
var log = require('./cdb.log');
var WindshaftError = require('./windshaft/error');

/**
 *
 * Creates a new Engine.
 * An engine is the core of a carto app.
 *
 * With the help of external services the engine will:
 *
 *  - Keep the state of the layers and dataviews.
 *  - Serialize the state and send requests to the server.
 *  - Parse the server response and update the internal models.
 *  - Notify errors or successful operations.
 *
 * @param {Object} params - The parameters to initialize the engine.
 * @param {string} params.apiKey - Api key used to be autenticate in the windshaft server.
 * @param {string} params.authToken - Token used to be autenticate in the windshaft server.
 * @param {string} params.username - Name of the user registered in the windshaft server.
 * @param {string} params.serverUrl - Url of the windshaft server.
 * @param {boolean} params.templateName - While we dont remove named maps we must explicitly say when the map is named. Defaults to false.
 * @param {boolean} params.statTag - Token used to get map view statistics.
 * @class
 * @api
 */
function Engine (params) {
  if (!params) throw new Error('new Engine() called with no paramters');
  this._isNamedMap = params.templateName !== undefined;

  this._windshaftSettings = {
    urlTemplate: params.serverUrl,
    userName: params.username,
    statTag: params.statTag,
    apiKey: params.apiKey,
    authToken: params.authToken,
    templateName: params.templateName
  };

  this._windshaftClient = new WindshaftClient(this._windshaftSettings);

  // This object will be responsible of triggering the engine events.
  this._eventEmmitter = _.extend({}, Backbone.Events);

  this._analysisPoller = new AnalysisPoller();
  this._layersCollection = new LayersCollection();
  this._dataviewsCollection = new DataviewsCollection();

  this._cartoLayerGroup = new CartoDBLayerGroup(
    { apiKey: params.apiKey, authToken: params.authToken },
    { layersCollection: this._layersCollection }
  );

  this._modelUpdater = new ModelUpdater({
    dataviewsCollection: this._dataviewsCollection,
    layerGroupModel: this._cartoLayerGroup,
    layersCollection: this._layersCollection
  });
}

/**
 * Return the cartoLayergroup attached to the engine
 */
Engine.prototype.getLayerGroup = function () {
  return this._cartoLayerGroup;
};

/**
 * Bind a callback function to an event. The callback will be invoked whenever the event is fired.
 *
 * @param {string} event - The name of the event that triggers the callback execution.
 * @param {function} callback - A function to be executed when the event is fired.
 * @param {function} [context] - The context value for this when the callback is invoked.
 *
 * @example
 *
 * // Define a callback to be executed once the map is reloaded.
 * function onReload(event) {
 *  console.log(event); // "reload-success"
 * }
 *
 * // Attach the callback to the RELOAD_SUCCESS event.
 * engine.on(Engine.Events.RELOAD_SUCCESS, onReload);
 *
 * // Call the reload method and wait.
 * engine.reload();
 *
 * @api
 */
Engine.prototype.on = function (event, callback, context) {
  this._eventEmmitter.on(event, callback, context);
};

/**
 * Remove a previously-bound callback function from an event.
 *
 * @param {string} event - The name of the event that triggers the callback execution.
 * @param {function} callback - A function callback to be removed when the event is fired.
 * @param {function} [context] - The context value for this when the callback is invoked.
 *
 * @example
 *
 * // Remove the the `displayMap` listener function so it wont be executed anymore when the engine fires the `load` event.
 * engine.off(Engine.Events.RELOAD_SUCCESS, onReload);
 *
 * @api
 */
Engine.prototype.off = function (event, callback, context) {
  this._eventEmmitter.off(event, callback, context);
};

/**
 * This is the most important function of the engine.
 * Generate a payload from the current state, send it to the windshaft server
 * and update the internal models with the server response.
 *
 * Once the response has arrived trigger a 'reload-succes' or 'reload-error' event.
 *
 * @param {string} options.sourceId - The sourceId triggering the reload event. This is usefull to prevent uneeded requests and save data.
 * @param {boolean} options.forceFetch - Forces dataviews to fetch data from server after a reload
 * @param {boolean} options.includeFilters - Boolean flag to control if the filters need to be added in the payload.
 *
 * @fires Engine#Engine:RELOAD_STARTED
 * @fires Engine#Engine:RELOAD_SUCCESS
 * @fires Engine#Engine:RELOAD_ERROR
 *
 * @api
 */
Engine.prototype.reload = function (options) {
  options = this._buildOptions(options);
  try {
    var params = this._buildParams(options.includeFilters);
    var payload = this._getSerializer().serialize(this._layersCollection, this._dataviewsCollection);
    var request = new Request(payload, params, options);

    this._eventEmmitter.trigger(Engine.Events.RELOAD_STARTED);
    this._windshaftClient.instantiateMap(request);
  } catch (error) {
    this._manageClientError(error, options);
  }
};

/**
 *
 * Add a layer to the engine layersCollection
 *
 * @param {layer} layer - A new layer to be added to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.addLayer = function (layer) {
  this._layersCollection.add(layer);
};

/**
 *
 * Remove a layer from the engine layersCollection
 *
 * @param {layer} layer - A new layer to be removed from the engine.
 *
 * @public
 * @api
 */
Engine.prototype.removeLayer = function (layer) {
  this._layersCollection.remove(layer);
};

/**
 *
 * Add a dataview to the engine dataviewsCollection
 *
 * @param {Dataview} dataview - A new dataview to be added to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.addDataview = function (dataview) {
  this._dataviewsCollection.add(dataview);
};

/**
 *
 * Remove a dataview from the engine dataviewsCollection
 *
 * @param {Dataview} dataview - The Dataview to be removed to the engine.
 *
 * @public
 * @api
 */
Engine.prototype.removeDataview = function (dataview) {
  this._dataviewsCollection.remove(dataview);
};

/**
 * Callback executed when the windhsaft client returns a successful response.
 * Update internal models and trigger a reload_sucess event.
 * @private
 */
Engine.prototype._onReloadSuccess = function (serverResponse, options) {
  var responseWrapper = new Response(this._windshaftSettings, serverResponse);
  this._modelUpdater.updateModels(responseWrapper, options.sourceId, options.forceFetch);
  this._restartAnalysisPolling();
  this._eventEmmitter.trigger(Engine.Events.RELOAD_SUCCESS);
  options.success && options.success();
};

/**
 * Callback executed when the windhsaft client returns a failed response.
 * Update internal models setting errores and trigger a reload_error event.
 * @private
 */
Engine.prototype._onReloadError = function (serverResponse, options) {
  var windshaftErrors = parseWindshaftErrors(serverResponse);
  var error = _.find(windshaftErrors, function (error) { return error.isGlobalError(); });
  this._modelUpdater.setErrors(windshaftErrors);
  this._eventEmmitter.trigger(Engine.Events.RELOAD_ERROR, error);
  options.error && options.error();
};

/**
 * Helper to get windhsaft request options.
 * @private
 */
Engine.prototype._buildOptions = function (options) {
  options = options || {};
  return _.extend({
    includeFilters: true,
    success: function (serverResponse) {
      this._onReloadSuccess(serverResponse, options);
    }.bind(this),
    error: function (serverResponse) {
      this._onReloadError(serverResponse, options);
    }.bind(this)
  }, _.pick(options, 'sourceId', 'forceFetch', 'includeFilters'));
};

/**
 * Helper to get windhsaft request parameters.
 * @param {boolean} includeFilters - Boolean flag to control if the filters need to be added in the payload.
 * @private
 */
Engine.prototype._buildParams = function (includeFilters) {
  var params = {
    stat_tag: this._windshaftSettings.statTag
  };
  if (includeFilters && !_.isEmpty(this._dataviewsCollection.getFilters())) {
    params.filters = this._dataviewsCollection.getFilters();
  }
  if (this._windshaftSettings.apiKey) {
    params.api_key = this._windshaftSettings.apiKey;
    return params;
  }
  if (this._windshaftSettings.authToken) {
    params.auth_token = this._windshaftSettings.authToken;
    return params;
  }
  console.error('Engine initialized with no apiKeys neither authToken');
};

/**
 * Reset the analysis nodes in the poller
 * @private
 */
Engine.prototype._restartAnalysisPolling = function () {
  var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection);
  this._analysisPoller.resetAnalysisNodes(analysisNodes);
};

/**
 * Get the instance of the serializer service depending on is an anonymous or a named map.
 * @private
 */
Engine.prototype._getSerializer = function () {
  return this._isNamedMap ? NamedMapSerializer : AnonymousMapSerializer;
};

/**
 * Manage and propagate the client error
 * @private
 */
Engine.prototype._manageClientError = function (error, options) {
  var windshaftError = new WindshaftError({
    message: error.message
  });
  this._modelUpdater.setErrors([windshaftError]);

  log.error(error.message);
  options.error && options.error();
};

/**
 * Events fired by the engine
 *
 * @readonly
 * @enum {string}
 * @api
 */
Engine.Events = {
  /**
   * Reload started event, fired every time the reload process starts.
   */
  RELOAD_STARTED: 'reload-started',
  /**
   * Reload success event, fired every time the reload function succeed.
   */
  RELOAD_SUCCESS: 'reload-success',
  /**
   * Reload success event, fired every time the reload function fails.
   */
  RELOAD_ERROR: 'reload-error'
};

module.exports = Engine;

/**
 * Reload started event, fired every time the reload process starts.
 *
 * @event Engine#Engine:RELOAD_STARTED
 * @type {string}
 * @api
 */

/**
  * Reload success event, fired every time the reload function succeed.
  *
  * @event Engine#Engine:RELOAD_SUCCESS
  * @type {string}
  * @api
  */

/**
  * Reload success event, fired every time the reload function fails.
  *
  * @event Engine#Engine:RELOAD_ERROR
  * @type {string}
  * @api
  */
<|endoftext|>


<commit_before>import ICESession from './ICESession';
import { importFromSDP, exportToSDP } from './lib/Intermediate';
import { convertRequestToIntermediate, convertIntermediateToRequest } from './lib/Protocol';

function applyStreamsCompatibility(content) {
    /* signal .streams as a=ssrc: msid */
    if (
        content.application.streams &&
        content.application.streams.length &&
        content.application.sources &&
        content.application.sources.length
    ) {
        const msid = content.application.streams[0];
        content.application.sources[0].parameters.push({
            key: 'msid',
            value: `${msid.id} ${msid.track}`
        });
        if (content.application.sourceGroups && content.application.sourceGroups.length > 0) {
            content.application.sources.push({
                parameters: [
                    {
                        key: 'cname',
                        value: content.application.sources[0].parameters[0].value
                    },
                    { key: 'msid', value: `${msid.id} ${msid.track}` }
                ],
                ssrc: content.application.sourceGroups[0].sources[1]
            });
        }
    }
}

export default class MediaSession extends ICESession {
    constructor(opts) {
        super(opts);

        this.pc.addEventListener('track', e => {
            this.onAddTrack(e.track, e.streams[0]);
        });

        if (opts.stream) {
            for (const track of opts.stream.getTracks()) {
                this.addTrack(track, opts.stream);
            }
        }

        this._ringing = false;
    }

    // ----------------------------------------------------------------
    // Session control methods
    // ----------------------------------------------------------------

    start(offerOptions, next) {
        this.state = 'pending';

        next = next || (() => undefined);

        this.role = 'initiator';
        this.offerOptions = offerOptions;
        this.pc
            .createOffer(offerOptions)
            .then(offer => {
                const json = importFromSDP(offer.sdp);
                const jingle = convertIntermediateToRequest(json, this.role);
                jingle.sessionId = this.sid;
                jingle.action = 'session-initate';
                jingle.contents.forEach(content => {
                    content.creator = 'initiator';
                    applyStreamsCompatibility(content);
                });

                this.send('session-initiate', jingle);

                return this.pc.setLocalDescription(offer).then(() => next());
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC offer', err);
                this.end('failed-application', true);
            });
    }

    accept(opts, next) {
        // support calling with accept(next) or accept(opts, next)
        if (arguments.length === 1 && typeof opts === 'function') {
            next = opts;
            opts = {};
        }
        next = next || (() => undefined);
        opts = opts || {};

        this._log('info', 'Accepted incoming session');

        this.state = 'active';

        this.role = 'responder';

        this.pc
            .createAnswer(opts)
            .then(answer => {
                const json = importFromSDP(answer.sdp);
                const jingle = convertIntermediateToRequest(json, this.role);
                jingle.sessionId = this.sid;
                jingle.action = 'session-accept';
                jingle.contents.forEach(content => {
                    content.creator = 'initiator';
                });
                this.send('session-accept', jingle);
                return this.pc.setLocalDescription(answer).then(() => next());
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC answer', err);
                this.end('failed-application');
            });
    }

    end(reason, silent) {
        this.pc.getReceivers().forEach(receiver => {
            this.onRemoveTrack(receiver.track);
        });
        this.pc.close();
        super.end(reason, silent);
    }

    ring() {
        this._log('info', 'Ringing on incoming session');
        this.ringing = true;
        this.send('session-info', { ringing: true });
    }

    mute(creator, name) {
        this._log('info', 'Muting', name);

        this.send('session-info', {
            mute: {
                creator,
                name
            }
        });
    }

    unmute(creator, name) {
        this._log('info', 'Unmuting', name);
        this.send('session-info', {
            unmute: {
                creator,
                name
            }
        });
    }

    hold() {
        this._log('info', 'Placing on hold');
        this.send('session-info', { hold: true });
    }

    resume() {
        this._log('info', 'Resuming from hold');
        this.send('session-info', { active: true });
    }

    // ----------------------------------------------------------------
    // Track control methods
    // ----------------------------------------------------------------

    addTrack(track, stream, cb) {
        if (this.pc.addTrack) {
            this.pc.addTrack(track, stream);
        } else {
            this.pc.addStream(stream, cb);
        }
        if (cb) {
            return cb();
        }
    }

    removeTrack(sender, cb) {
        this.pc.removeTrack(sender);
        if (cb) {
            return cb();
        }
    }

    // ----------------------------------------------------------------
    // Track event handlers
    // ----------------------------------------------------------------

    onAddTrack(track, stream) {
        this._log('info', 'Track added');
        this.emit('peerTrackAdded', this, track, stream);
    }

    onRemoveTrack(track) {
        this._log('info', 'Track removed');
        this.emit('peerTrackRemoved', this, track);
    }

    // ----------------------------------------------------------------
    // Jingle action handers
    // ----------------------------------------------------------------

    onSessionInitiate(changes, cb) {
        this._log('info', 'Initiating incoming session');

        this.state = 'pending';
        this.role = 'responder';

        const json = convertRequestToIntermediate(changes, this.peerRole);
        json.media.forEach(media => {
            if (!media.streams) {
                media.streams = [{ stream: 'legacy', track: media.kind }];
            }
        });

        const sdp = exportToSDP(json);
        this.pc
            .setRemoteDescription({ type: 'offer', sdp })
            .then(() => {
                if (cb) {
                    return cb();
                }
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC answer', err);
                if (cb) {
                    return cb({ condition: 'general-error' });
                }
            });
    }

    onSessionTerminate(changes, cb) {
        for (const receiver of this.pc.getReceivers()) {
            this.onRemoveTrack(receiver.track);
        }
        super.end(changes, cb);
    }

    onSessionInfo(info, cb) {
        if (info.ringing) {
            this._log('info', 'Outgoing session is ringing');
            this.ringing = true;
            this.emit('ringing', this);
            return cb();
        }

        if (info.hold) {
            this._log('info', 'On hold');
            this.emit('hold', this);
            return cb();
        }

        if (info.active) {
            this._log('info', 'Resuming from hold');
            this.emit('resumed', this);
            return cb();
        }

        if (info.mute) {
            this._log('info', 'Muting', info.mute);
            this.emit('mute', this, info.mute);
            return cb();
        }

        if (info.unmute) {
            this._log('info', 'Unmuting', info.unmute);
            this.emit('unmute', this, info.unmute);
            return cb();
        }

        return cb();
    }

    get ringing() {
        return this._ringing;
    }
    set ringing(value) {
        if (value !== this._ringing) {
            this._ringing = value;
            this.emit('change:ringing', value);
        }
    }

    get streams() {
        if (this.pc.signalingState !== 'closed') {
            return this.pc.getRemoteStreams();
        }
        return [];
    }
}
<commit_msg>Remove duplicate peer connection close() call.<commit_after>import ICESession from './ICESession';
import { importFromSDP, exportToSDP } from './lib/Intermediate';
import { convertRequestToIntermediate, convertIntermediateToRequest } from './lib/Protocol';

function applyStreamsCompatibility(content) {
    /* signal .streams as a=ssrc: msid */
    if (
        content.application.streams &&
        content.application.streams.length &&
        content.application.sources &&
        content.application.sources.length
    ) {
        const msid = content.application.streams[0];
        content.application.sources[0].parameters.push({
            key: 'msid',
            value: `${msid.id} ${msid.track}`
        });
        if (content.application.sourceGroups && content.application.sourceGroups.length > 0) {
            content.application.sources.push({
                parameters: [
                    {
                        key: 'cname',
                        value: content.application.sources[0].parameters[0].value
                    },
                    { key: 'msid', value: `${msid.id} ${msid.track}` }
                ],
                ssrc: content.application.sourceGroups[0].sources[1]
            });
        }
    }
}

export default class MediaSession extends ICESession {
    constructor(opts) {
        super(opts);

        this.pc.addEventListener('track', e => {
            this.onAddTrack(e.track, e.streams[0]);
        });

        if (opts.stream) {
            for (const track of opts.stream.getTracks()) {
                this.addTrack(track, opts.stream);
            }
        }

        this._ringing = false;
    }

    // ----------------------------------------------------------------
    // Session control methods
    // ----------------------------------------------------------------

    start(offerOptions, next) {
        this.state = 'pending';

        next = next || (() => undefined);

        this.role = 'initiator';
        this.offerOptions = offerOptions;
        this.pc
            .createOffer(offerOptions)
            .then(offer => {
                const json = importFromSDP(offer.sdp);
                const jingle = convertIntermediateToRequest(json, this.role);
                jingle.sessionId = this.sid;
                jingle.action = 'session-initate';
                jingle.contents.forEach(content => {
                    content.creator = 'initiator';
                    applyStreamsCompatibility(content);
                });

                this.send('session-initiate', jingle);

                return this.pc.setLocalDescription(offer).then(() => next());
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC offer', err);
                this.end('failed-application', true);
            });
    }

    accept(opts, next) {
        // support calling with accept(next) or accept(opts, next)
        if (arguments.length === 1 && typeof opts === 'function') {
            next = opts;
            opts = {};
        }
        next = next || (() => undefined);
        opts = opts || {};

        this._log('info', 'Accepted incoming session');

        this.state = 'active';

        this.role = 'responder';

        this.pc
            .createAnswer(opts)
            .then(answer => {
                const json = importFromSDP(answer.sdp);
                const jingle = convertIntermediateToRequest(json, this.role);
                jingle.sessionId = this.sid;
                jingle.action = 'session-accept';
                jingle.contents.forEach(content => {
                    content.creator = 'initiator';
                });
                this.send('session-accept', jingle);
                return this.pc.setLocalDescription(answer).then(() => next());
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC answer', err);
                this.end('failed-application');
            });
    }

    end(reason, silent) {
        this.pc.getReceivers().forEach(receiver => {
            this.onRemoveTrack(receiver.track);
        });
        super.end(reason, silent);
    }

    ring() {
        this._log('info', 'Ringing on incoming session');
        this.ringing = true;
        this.send('session-info', { ringing: true });
    }

    mute(creator, name) {
        this._log('info', 'Muting', name);

        this.send('session-info', {
            mute: {
                creator,
                name
            }
        });
    }

    unmute(creator, name) {
        this._log('info', 'Unmuting', name);
        this.send('session-info', {
            unmute: {
                creator,
                name
            }
        });
    }

    hold() {
        this._log('info', 'Placing on hold');
        this.send('session-info', { hold: true });
    }

    resume() {
        this._log('info', 'Resuming from hold');
        this.send('session-info', { active: true });
    }

    // ----------------------------------------------------------------
    // Track control methods
    // ----------------------------------------------------------------

    addTrack(track, stream, cb) {
        if (this.pc.addTrack) {
            this.pc.addTrack(track, stream);
        } else {
            this.pc.addStream(stream, cb);
        }
        if (cb) {
            return cb();
        }
    }

    removeTrack(sender, cb) {
        this.pc.removeTrack(sender);
        if (cb) {
            return cb();
        }
    }

    // ----------------------------------------------------------------
    // Track event handlers
    // ----------------------------------------------------------------

    onAddTrack(track, stream) {
        this._log('info', 'Track added');
        this.emit('peerTrackAdded', this, track, stream);
    }

    onRemoveTrack(track) {
        this._log('info', 'Track removed');
        this.emit('peerTrackRemoved', this, track);
    }

    // ----------------------------------------------------------------
    // Jingle action handers
    // ----------------------------------------------------------------

    onSessionInitiate(changes, cb) {
        this._log('info', 'Initiating incoming session');

        this.state = 'pending';
        this.role = 'responder';

        const json = convertRequestToIntermediate(changes, this.peerRole);
        json.media.forEach(media => {
            if (!media.streams) {
                media.streams = [{ stream: 'legacy', track: media.kind }];
            }
        });

        const sdp = exportToSDP(json);
        this.pc
            .setRemoteDescription({ type: 'offer', sdp })
            .then(() => {
                if (cb) {
                    return cb();
                }
            })
            .catch(err => {
                this._log('error', 'Could not create WebRTC answer', err);
                if (cb) {
                    return cb({ condition: 'general-error' });
                }
            });
    }

    onSessionTerminate(changes, cb) {
        for (const receiver of this.pc.getReceivers()) {
            this.onRemoveTrack(receiver.track);
        }
        super.end(changes, cb);
    }

    onSessionInfo(info, cb) {
        if (info.ringing) {
            this._log('info', 'Outgoing session is ringing');
            this.ringing = true;
            this.emit('ringing', this);
            return cb();
        }

        if (info.hold) {
            this._log('info', 'On hold');
            this.emit('hold', this);
            return cb();
        }

        if (info.active) {
            this._log('info', 'Resuming from hold');
            this.emit('resumed', this);
            return cb();
        }

        if (info.mute) {
            this._log('info', 'Muting', info.mute);
            this.emit('mute', this, info.mute);
            return cb();
        }

        if (info.unmute) {
            this._log('info', 'Unmuting', info.unmute);
            this.emit('unmute', this, info.unmute);
            return cb();
        }

        return cb();
    }

    get ringing() {
        return this._ringing;
    }
    set ringing(value) {
        if (value !== this._ringing) {
            this._ringing = value;
            this.emit('change:ringing', value);
        }
    }

    get streams() {
        if (this.pc.signalingState !== 'closed') {
            return this.pc.getRemoteStreams();
        }
        return [];
    }
}
<|endoftext|>


<commit_before>var STATUS_CHARS = 140;
var RESERVED_CHARS = 23;
var URL_CHARS = 23;
var AMAZON_DOMAINS = ['amazon.com', 'www.amazon.com', 'amzn.to'];

var $status_wrapper = null;
var $display_quote = null;
var $display_attribution = null;
var $display_status = null;
var $count = null;

var $poster = null;
var $logo_wrapper = null;

var $comment = null;
var $quote = null;
var $source = null;
var $fontSize = null;
var $url = null;

var $login = null;
var $tweet = null;
var $save = null;

var quotes = [
    {
        'comment': 'One of my favorite LOTR quotes.',
        'quote': 'Not all those who wander are lost.',
        'source': 'J.R.R. Tolkien, The Lord of the Rings',
        'url': 'http://www.amazon.com/Lord-Rings-50th-Anniversary-Vol/dp/0618640150/ref=sr_1_3?s=books&ie=UTF8&qid=1408030941&sr=1-3&keywords=lord+of+the+rings'
    }
];

/*
 * On page load.
 */
var onDocumentReady = function() {
    $status_wrapper = $('.status');
    $display_quote = $('.poster blockquote p');
    $display_attribution = $('.attribution');
    $display_status = $('.status .text');
    $count = $('.count');

    $poster = $('.poster');
    $logo_wrapper = $('.logo-wrapper');

    $comment = $('#comment');
    $quote = $('#quote');
    $source = $('#source');
    $fontSize = $('#fontsize');
    $url = $('#url');

    $login = $('#login');
    $save = $('#save');
    $tweet = $('#tweet');

    // Event binding
    $quote.on('keyup', onQuoteKeyUp);
    $source.on('keyup', onSourceKeyUp);
    $comment.on('keyup', onCommentKeyUp);
    $fontSize.on('change', onFontSizeChange);
    $url.on('keyup', onUrlKeyUp);

    $login.on('click', onLoginClick);
    $tweet.on('click', onTweetClick);
    $save.on('click', onSaveClick);

    // Setup initial quote
    var quote = quotes[Math.floor(Math.random() * quotes.length)];

    if (quote.size) {
        adjustFontSize(quote.size);
    }

    $quote.val(quote.quote);
    $source.val(quote.source);
    $comment.val(quote.comment);
    $url.val(quote.url);

    $quote.trigger('keyup');
    $source.trigger('keyup');
    $comment.trigger('keyup');
}

/*
 * Smarten quotes.
 */
var smarten = function(a) {
    // opening singles
    a = a.replace(/(^|[-\u2014\s(\["])'/g, "$1\u2018");
    // closing singles & apostrophes
    a = a.replace(/'/g, "\u2019");
    // opening doubles
    a = a.replace(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c");
    // closing doubles
    a = a.replace(/"/g, "\u201d");
    // em-dashes
    a = a.replace(/--/g, "\u2014");
    // full spaces wrapping em dash
    a = a.replace(/ \u2014 /g, "\u2009\u2014\u2009");

    return a;
}

/*
 * POST without a pre-generated form.
 */
var post = function(path, params) {
    var $form = $('<form></form>');
    $form.attr('method', 'post');
    $form.attr('action', path);

    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            var $hiddenField = $('<input />');
            $hiddenField.attr('type', 'hidden');
            $hiddenField.attr('name', key);
            $hiddenField.attr('value', params[key]);

            $form.append($hiddenField);
         }
    }

    $('body').append($form);
    $form.submit();
}

/*
 * Slugify a string for a filename.
 */
var slugify = function(text){
    return text
        .toLowerCase()
        .replace(/[^\w ]+/g,'')
        .replace(/ +/g,'-');
}

/*
 * Gets data for the image calls a callback when its ready
 */
var getImage = function(callback) {
    if (($source.offset().top + $source.height()) > $logo_wrapper.offset().top){
        alert('Your quote doesn\'t quite fit. Shorten the text or choose a smaller font-size.');
        return;
    }

    html2canvas($poster, {
        onrendered: function(canvas) {
            var dataUrl = canvas.toDataURL();

            callback(dataUrl);
        }
    });

}

/*
 * Tweet the image.
 */
var tweet = function(dataUrl) {
    var status = $display_status.val(status);

    ga('send', 'event', 'pixelcite', 'tweet');

    post('/post/', {
        'status': status,
        'image': dataUrl.split(',')[1]
    });
}

/*
 * Downloads the image.
 */
var saveImage = function(dataUrl) {
    ga('send', 'event', 'pixelcite', 'save-image');

    var quote = $('blockquote').text().split(' ', 5);
    var filename = slugify(quote.join(' '));

    var a = $('').attr('href', dataUrl).attr('download', 'quote-' + filename + '.png').appendTo('body');

    a[0].click();

    a.remove();

    $('#download').attr('href', dataUrl).attr('target', '_blank');
    $('#download').trigger('click');
}

var adjustFontSize = function(size) {
    var fontSize = size.toString() + 'px';

    $poster.css('font-size', fontSize);

    if ($fontSize.val() !== size){
        $fontSize.val(size);
    };
}

/*
 * Process urls and apply affiliate codes.
 */
var processUrl = function(url) {
    if (url.indexOf('http') != 0) {
        url = 'http://' + url;
    }

    var parser = document.createElement('a');
    parser.href = url;

    if (AMAZON_DOMAINS.indexOf(parser.hostname) >= 0) {
        var pixelciteTag = 'tag=' + APP_CONFIG.AMAZON_AFFILIATE_TAG;

        var tag = /(tag\=.*?)[&\W]/;
        var match = url.match(tag);

        if (match) {
            var existingTag = match[1];

            return url.replace(existingTag, pixelciteTag);
        }

        if (parser.search) {
            return url + '&' + pixelciteTag;
        }

        return url + '?' + pixelciteTag;
    }

    return url;
}

var updateAttribution = function() {
    var source = $source.val();
    var attr = '';

    if (source) {
        attr += '— ' + source;
    }

    $display_attribution.html(attr);
}

var updateStatus = function() {
    var status = $comment.val();
    var url = $url.val();

    if (url) {
        status += ' ' + processUrl(url);
    }

    $display_status.text(status);

    updateCount();
}

var updateCount = function() {
    var count = $comment.val().length;

    var url = $url.val();

    if (url) {
        count += URL_CHARS;
    }

    var max = STATUS_CHARS - RESERVED_CHARS;
    var remaining = max - count;

    $count.text(remaining);

    $count.toggleClass('negative', remaining < 0);
}

var onCommentKeyUp = function() {
    updateStatus();
}

var onQuoteKeyUp = function() {
    $display_quote.text(smarten($(this).val()));
}

var onSourceKeyUp = function() {
    updateAttribution();
}

var onUrlKeyUp = function () {
    updateStatus();
}

var onFontSizeChange = function() {
    adjustFontSize($(this).val());
}

var onLoginClick = function() {
    ga('send', 'event', 'pixelcite', 'login');

    window.location.href = '/authenticate/';
}

var onTweetClick = function() {
    getImage(tweet);
}

var onSaveClick =  function() {
    getImage(saveImage);
}

$(onDocumentReady);
<commit_msg>Check tweet length before posting. Closes #40.<commit_after>var STATUS_CHARS = 140;
var RESERVED_CHARS = 23;
var URL_CHARS = 23;
var AMAZON_DOMAINS = ['amazon.com', 'www.amazon.com', 'amzn.to'];

var $status_wrapper = null;
var $display_quote = null;
var $display_attribution = null;
var $display_status = null;
var $count = null;

var $poster = null;
var $logo_wrapper = null;

var $comment = null;
var $quote = null;
var $source = null;
var $fontSize = null;
var $url = null;

var $login = null;
var $tweet = null;
var $save = null;

var quotes = [
    {
        'comment': 'One of my favorite LOTR quotes.',
        'quote': 'Not all those who wander are lost.',
        'source': 'J.R.R. Tolkien, The Lord of the Rings',
        'url': 'http://www.amazon.com/Lord-Rings-50th-Anniversary-Vol/dp/0618640150/ref=sr_1_3?s=books&ie=UTF8&qid=1408030941&sr=1-3&keywords=lord+of+the+rings'
    }
];

/*
 * On page load.
 */
var onDocumentReady = function() {
    $status_wrapper = $('.status');
    $display_quote = $('.poster blockquote p');
    $display_attribution = $('.attribution');
    $display_status = $('.status .text');
    $count = $('.count');

    $poster = $('.poster');
    $logo_wrapper = $('.logo-wrapper');

    $comment = $('#comment');
    $quote = $('#quote');
    $source = $('#source');
    $fontSize = $('#fontsize');
    $url = $('#url');

    $login = $('#login');
    $save = $('#save');
    $tweet = $('#tweet');

    // Event binding
    $quote.on('keyup', onQuoteKeyUp);
    $source.on('keyup', onSourceKeyUp);
    $comment.on('keyup', onCommentKeyUp);
    $fontSize.on('change', onFontSizeChange);
    $url.on('keyup', onUrlKeyUp);

    $login.on('click', onLoginClick);
    $tweet.on('click', onTweetClick);
    $save.on('click', onSaveClick);

    // Setup initial quote
    var quote = quotes[Math.floor(Math.random() * quotes.length)];

    if (quote.size) {
        adjustFontSize(quote.size);
    }

    $quote.val(quote.quote);
    $source.val(quote.source);
    $comment.val(quote.comment);
    $url.val(quote.url);

    $quote.trigger('keyup');
    $source.trigger('keyup');
    $comment.trigger('keyup');
}

/*
 * Smarten quotes.
 */
var smarten = function(a) {
    // opening singles
    a = a.replace(/(^|[-\u2014\s(\["])'/g, "$1\u2018");
    // closing singles & apostrophes
    a = a.replace(/'/g, "\u2019");
    // opening doubles
    a = a.replace(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c");
    // closing doubles
    a = a.replace(/"/g, "\u201d");
    // em-dashes
    a = a.replace(/--/g, "\u2014");
    // full spaces wrapping em dash
    a = a.replace(/ \u2014 /g, "\u2009\u2014\u2009");

    return a;
}

/*
 * POST without a pre-generated form.
 */
var post = function(path, params) {
    var $form = $('<form></form>');
    $form.attr('method', 'post');
    $form.attr('action', path);

    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            var $hiddenField = $('<input />');
            $hiddenField.attr('type', 'hidden');
            $hiddenField.attr('name', key);
            $hiddenField.attr('value', params[key]);

            $form.append($hiddenField);
         }
    }

    $('body').append($form);
    $form.submit();
}

/*
 * Slugify a string for a filename.
 */
var slugify = function(text){
    return text
        .toLowerCase()
        .replace(/[^\w ]+/g,'')
        .replace(/ +/g,'-');
}

/*
 * Gets data for the image calls a callback when its ready
 */
var getImage = function(callback) {
    if (($source.offset().top + $source.height()) > $logo_wrapper.offset().top){
        alert('Your quote doesn\'t quite fit. Shorten the text or choose a smaller font-size.');
        return;
    }

    html2canvas($poster, {
        onrendered: function(canvas) {
            var dataUrl = canvas.toDataURL();

            callback(dataUrl);
        }
    });

}

/*
 * Tweet the image.
 */
var tweet = function(dataUrl) {
    var remaining = getRemainingChars();

    if (remaining < 0) {
        alert('Sorry, your status update is too long to post to Twitter.');

        return;
    }

    var status = $display_status.val(status);

    ga('send', 'event', 'pixelcite', 'tweet');

    post('/post/', {
        'status': status,
        'image': dataUrl.split(',')[1]
    });
}

/*
 * Downloads the image.
 */
var saveImage = function(dataUrl) {
    ga('send', 'event', 'pixelcite', 'save-image');

    var quote = $('blockquote').text().split(' ', 5);
    var filename = slugify(quote.join(' '));

    var a = $('').attr('href', dataUrl).attr('download', 'quote-' + filename + '.png').appendTo('body');

    a[0].click();

    a.remove();

    $('#download').attr('href', dataUrl).attr('target', '_blank');
    $('#download').trigger('click');
}

var adjustFontSize = function(size) {
    var fontSize = size.toString() + 'px';

    $poster.css('font-size', fontSize);

    if ($fontSize.val() !== size){
        $fontSize.val(size);
    };
}

/*
 * Process urls and apply affiliate codes.
 */
var processUrl = function(url) {
    if (url.indexOf('http') != 0) {
        url = 'http://' + url;
    }

    var parser = document.createElement('a');
    parser.href = url;

    if (AMAZON_DOMAINS.indexOf(parser.hostname) >= 0) {
        var pixelciteTag = 'tag=' + APP_CONFIG.AMAZON_AFFILIATE_TAG;

        var tag = /(tag\=.*?)[&\W]/;
        var match = url.match(tag);

        if (match) {
            var existingTag = match[1];

            return url.replace(existingTag, pixelciteTag);
        }

        if (parser.search) {
            return url + '&' + pixelciteTag;
        }

        return url + '?' + pixelciteTag;
    }

    return url;
}

/*
 * Get count of characters remaining in status.
 */
var getRemainingChars = function() {
    var count = $comment.val().length;

    var url = $url.val();

    if (url) {
        count += URL_CHARS;
    }

    var max = STATUS_CHARS - RESERVED_CHARS;

    return max - count;
}

var updateAttribution = function() {
    var source = $source.val();
    var attr = '';

    if (source) {
        attr += '— ' + source;
    }

    $display_attribution.html(attr);
}

var updateStatus = function() {
    var status = $comment.val();
    var url = $url.val();

    if (url) {
        status += ' ' + processUrl(url);
    }

    $display_status.text(status);

    updateCount();
}

var updateCount = function() {
    var remaining = getRemainingChars();

    $count.text(remaining);

    $count.toggleClass('negative', remaining < 0);
}

var onCommentKeyUp = function() {
    updateStatus();
}

var onQuoteKeyUp = function() {
    $display_quote.text(smarten($(this).val()));
}

var onSourceKeyUp = function() {
    updateAttribution();
}

var onUrlKeyUp = function () {
    updateStatus();
}

var onFontSizeChange = function() {
    adjustFontSize($(this).val());
}

var onLoginClick = function() {
    ga('send', 'event', 'pixelcite', 'login');

    window.location.href = '/authenticate/';
}

var onTweetClick = function() {
    getImage(tweet);
}

var onSaveClick =  function() {
    getImage(saveImage);
}

$(onDocumentReady);
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';

import findIndex from 'lodash/findIndex';

// Redux
import { connect } from 'react-redux';

// Components
import Field from 'components/form/Field';
import Input from 'components/form/Input';
import Select from 'components/form/SelectInput';

import { getInteractions, modifyInteractions } from 'components/admin/layers/form/interactions/interactions-actions';

import { FORM_ELEMENTS, FORMAT } from 'components/admin/layers/form/constants';

const DATA_FORMATS = {
  string: [],
  number: [
    { label: '00000', value: '00000' },
    { label: '0,0', value: '0,0' },
    { label: '0a', value: '0a' }
  ],
  date: [
    { label: 'YYYY/MM/DD', value: 'YYYY/MM/DD' },
    { label: 'YYYY', value: 'YYYY' },
    { label: 'MM', value: 'MM' },
    { label: 'DD', value: 'DD' }
  ]
};

class InteractionsComponent extends PureComponent {
  componentWillMount() {
    this.props.dispatch(getInteractions({ ...this.props }));
  }

  addInteractions(options) {
    const { interactions } = this.props;

    // Check if we are removing interactions, then remove the reference(es) to it
    if (options.length < interactions.added.length) {
      let interactionsRemoved = interactions.added;
      options.map((item) => {
        interactionsRemoved = interactionsRemoved.filter(t => t.column !== item);
      });
      interactionsRemoved.forEach(interaction => FORM_ELEMENTS.removeInteraction(interaction));
    }

    // Remove layer if its not in options
    if (interactions.added) {
      interactions.added = interactions.added
        .filter(item => options.includes(item.column));
    }

    if (!interactions.added || options.length > interactions.added.length) {
      const optionSelected = options[options.length - 1];
      const selected = interactions.available.find(item => item.label === optionSelected);

      interactions.added.push({
        column: selected.label,
        format: null,
        prefix: '',
        property: '',
        suffix: '',
        type: selected.type
      });
    }

    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  editInteraction(data) {
    const { interactions } = this.props;

    if (data.key.toLowerCase() === 'label') {
      data.field.property = data.value;
    } else {
      data.field[data.key.toLowerCase()] = data.value;
    }
    interactions.added[findIndex(interactions.added, data.field)] =
      Object.assign({}, data.field);
    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  removeInteraction(interaction) {
    const { interactions } = this.props;
    interactions.added = interactions.added.filter(item => item.column !== interaction.column);

    // Remove interaction references from validation
    FORM_ELEMENTS.removeInteraction(interaction);

    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  renderInteractionFields(data) {
    return ['Field', 'Label', 'Prefix', 'Suffix'].map((label) => {
      const validations = label === 'Label' ? ['required'] : [];
      return (
        <Field
          key={data.column + label}
          ref={(c) => { if (c) FORM_ELEMENTS.elements[label.toLowerCase() + data.column] = c; }}
          onChange={value => this.editInteraction({ value, key: label, field: data })}
          validations={validations}
          properties={{
            name: label.toLowerCase() + data.column,
            label,
            type: 'text',
            disabled: /Field/.test(label),
            required: /Label/.test(label),
            default: data[FORMAT.resolveKey(label)]
          }}
        >
          {Input}
        </Field>
      );
    });
  }

  renderFormatField(data) {
    return (
      <Field
        key={`${data.column}format`}
        ref={(c) => { if (c) FORM_ELEMENTS.elements[`format${data.column}`] = c; }}
        onChange={value => this.editInteraction({ value, key: 'format', field: data })}
        options={DATA_FORMATS[data.type]}
        properties={{
          name: `${data.column}format`,
          label: 'Format',
          type: 'text',
          disabled: /string/.test(data.type),
          default: /string/.test(data.type) ? 'string' : null
        }}
      >
        {Select}
      </Field>
    );
  }

  render() {
    const { interactions } = this.props;

    return (

          {interactions.available &&
          <Field
            options={interactions.available}
            onChange={value => this.addInteractions(value)}
            properties={{
              name: 'selected_columns',
              label: 'Add interactions',
              type: 'text',
              removeSelected: true,
              multi: true,
              value: interactions.added ? FORMAT.options(interactions.added) : [],
              default: interactions.added ? FORMAT.options(interactions.added) : []
            }}
          >
            {Select}
          </Field>}

          {interactions.added.length ?
Interactions ({interactions.added.length})
 : null}

          {interactions.added &&
            interactions.added.map((data) => {
              return (

                  {this.renderInteractionFields(data)}
                  {this.renderFormatField(data)}
                  <button type="button" className="c-btn" onClick={() => this.removeInteraction(data)}>Remove</button>

              );
            })}

    );
  }
}

const mapStateToProps = state => ({
  user: state.user,
  interactions: state.interactions
});

InteractionsComponent.propTypes = {
  dispatch: PropTypes.func.isRequired,
  interactions: PropTypes.object.isRequired,
  form: PropTypes.object.isRequired
};

InteractionsComponent.defaultProps = {

};

export default connect(mapStateToProps, null)(InteractionsComponent);<commit_msg>layer interactivity, add default format if its set to select<commit_after>import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';

import findIndex from 'lodash/findIndex';

// Redux
import { connect } from 'react-redux';

// Components
import Field from 'components/form/Field';
import Input from 'components/form/Input';
import Select from 'components/form/SelectInput';

import { getInteractions, modifyInteractions } from 'components/admin/layers/form/interactions/interactions-actions';

import { FORM_ELEMENTS, FORMAT } from 'components/admin/layers/form/constants';

const DATA_FORMATS = {
  number: [
    { label: '00000', value: '00000' },
    { label: '0,0', value: '0,0' },
    { label: '0a', value: '0a' }
  ],
  date: [
    { label: 'YYYY/MM/DD', value: 'YYYY/MM/DD' },
    { label: 'YYYY', value: 'YYYY' },
    { label: 'MM', value: 'MM' },
    { label: 'DD', value: 'DD' }
  ]
};

class InteractionsComponent extends PureComponent {
  componentWillMount() {
    this.props.dispatch(getInteractions({ ...this.props }));
  }

  addInteractions(options) {
    const { interactions } = this.props;

    // Check if we are removing interactions, then remove the reference(es) to it
    if (options.length < interactions.added.length) {
      let interactionsRemoved = interactions.added;
      options.map((item) => {
        interactionsRemoved = interactionsRemoved.filter(t => t.column !== item);
      });
      interactionsRemoved.forEach(interaction => FORM_ELEMENTS.removeInteraction(interaction));
    }

    // Remove layer if its not in options
    if (interactions.added) {
      interactions.added = interactions.added
        .filter(item => options.includes(item.column));
    }

    if (!interactions.added || options.length > interactions.added.length) {
      const optionSelected = options[options.length - 1];
      const selected = interactions.available.find(item => item.label === optionSelected);

      interactions.added.push({
        column: selected.label,
        format: null,
        prefix: '',
        property: '',
        suffix: '',
        type: selected.type
      });
    }

    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  editInteraction(data) {
    const { interactions } = this.props;

    if (data.key.toLowerCase() === 'label') {
      data.field.property = data.value;
    } else {
      data.field[data.key.toLowerCase()] = data.value;
    }
    interactions.added[findIndex(interactions.added, data.field)] =
      Object.assign({}, data.field);
    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  removeInteraction(interaction) {
    const { interactions } = this.props;
    interactions.added = interactions.added.filter(item => item.column !== interaction.column);

    // Remove interaction references from validation
    FORM_ELEMENTS.removeInteraction(interaction);

    this.props.dispatch(modifyInteractions({ ...this.props }, interactions.added));
  }

  renderInteractionFields(data) {
    return ['Field', 'Label', 'Prefix', 'Suffix'].map((label) => {
      const validations = label === 'Label' ? ['required'] : [];
      return (
        <Field
          key={data.column + label}
          ref={(c) => { if (c) FORM_ELEMENTS.elements[label.toLowerCase() + data.column] = c; }}
          onChange={value => this.editInteraction({ value, key: label, field: data })}
          validations={validations}
          properties={{
            name: label.toLowerCase() + data.column,
            label,
            type: 'text',
            disabled: /Field/.test(label),
            required: /Label/.test(label),
            default: data[FORMAT.resolveKey(label)]
          }}
        >
          {Input}
        </Field>
      );
    });
  }

  renderFormatField(data) {
    return (
      <Field
        key={`${data.column}format`}
        ref={(c) => { if (c) FORM_ELEMENTS.elements[`format${data.column}`] = c; }}
        onChange={value => this.editInteraction({ value, key: 'format', field: data })}
        options={DATA_FORMATS[data.type]}
        properties={{
          name: `${data.column}format`,
          label: 'Format',
          type: 'text',
          disabled: /string/.test(data.type),
          default: data.format
        }}
      >
        {Select}
      </Field>
    );
  }

  render() {
    const { interactions } = this.props;

    return (

          {interactions.available &&
          <Field
            options={interactions.available}
            onChange={value => this.addInteractions(value)}
            properties={{
              name: 'selected_columns',
              label: 'Add interactions',
              type: 'text',
              removeSelected: true,
              multi: true,
              value: interactions.added ? FORMAT.options(interactions.added) : [],
              default: interactions.added ? FORMAT.options(interactions.added) : []
            }}
          >
            {Select}
          </Field>}

          {interactions.added.length ?
Interactions ({interactions.added.length})
 : null}

          {interactions.added &&
            interactions.added.map((data) => {
              return (

                  {this.renderInteractionFields(data)}
                  {this.renderFormatField(data)}
                  <button type="button" className="c-btn" onClick={() => this.removeInteraction(data)}>Remove</button>

              );
            })}

    );
  }
}

const mapStateToProps = state => ({
  user: state.user,
  interactions: state.interactions
});

InteractionsComponent.propTypes = {
  dispatch: PropTypes.func.isRequired,
  interactions: PropTypes.object.isRequired,
  form: PropTypes.object.isRequired
};

InteractionsComponent.defaultProps = {

};

export default connect(mapStateToProps, null)(InteractionsComponent);<|endoftext|>


<commit_before>/* Version 0.1.0 */
(function($){
  $.fn.simpleSteps = function (){
    var stepsContainer = this;
    var currentStep, nextButtons, steps, validations;

    currentStep = 0;

    validations = {
      "presence": validatePresence
    };

    nextButtons = $('.next-step-button');

    function init(){
      steps = $(stepsContainer).find('.step');
      nextStep();
    }

    function nextStep() {
      var i;
      for(i=0; i < steps.length; i++){
        if( i !== currentStep){
          $(steps[i]).hide();
        }else {
          $(steps[i]).show('slow');
        }
      }
      currentStep += 1;
    }

    function validatePresence(element){
      var i;
      var elements = $(element).find('.validate.presence');
      var isValid = true;
      for(i=0; i < elements.length; i++){
        $(elements[i]).removeClass('error');
        if(elements[i].value === ""){
          isValid = false;
          $(elements[i]).addClass('error');
        }
      }
      return isValid;
    }

    $('.next-step-button').on('click', function(e){
      var key;
      for(key in validations){
        if(validations[key](steps[currentStep-1])){
          nextStep();
        }
      }
      return false;
    });

    $('.prev-step-button').on('click',function(){
      currentStep -= 2;
      nextStep();
      return false;
    });

    init();

    return stepsContainer;
  };
}(jQuery));
<commit_msg>add options<commit_after>/* Version 0.1.0 */
(function($){
  $.fn.simpleSteps = function (options){
    options = $.extend({
      stepClass           : '.step',
      animationSpeed      : 500
    }, options);

    var stepsContainer = this;

    var currentStep, nextButtons, steps, validations;

    currentStep = 0;

    validations = {
      "presence": validatePresence
    };

    nextButtons = $('.next-step-button');

    function init(){
      steps = $(stepsContainer).find('.step');
      nextStep();
    }

    function nextStep() {
      var i;
      for(i=0; i < steps.length; i++){
        if( i !== currentStep){
          $(steps[i]).hide();
        }else {
          $(steps[i]).show('slow');
        }
      }
      currentStep += 1;
    }

    function validatePresence(element){
      var i;
      var elements = $(element).find('.validate.presence');
      var isValid = true;
      for(i=0; i < elements.length; i++){
        $(elements[i]).removeClass('error');
        if(elements[i].value === ""){
          isValid = false;
          $(elements[i]).addClass('error');
        }
      }
      return isValid;
    }

    $('.next-step-button').on('click', function(e){
      var key;
      for(key in validations){
        if(validations[key](steps[currentStep-1])){
          nextStep();
        }
      }
      return false;
    });

    $('.prev-step-button').on('click',function(){
      currentStep -= 2;
      nextStep();
      return false;
    });

    init();

    return stepsContainer;
  };
}(jQuery));
<|endoftext|>