<commit_before>/* eslint-disable
camelcase
 */

const autobahn = require('autobahn');
const ee = require('event-emitter');
/**
 * Controller class for managing connection interaction.
 */
module.exports = class Connection {

  constructor(options) {
    this.settings = Object.assign({
      // ITSL connection parameters.
      apiUrl: 'https://api.itslanguage.nl',
      oAuth2Token: null,
      wsUrl: null,
      wsToken: null
    }, options);
    Connection._sdkCompatibility();
    this._analysisId = null;
    this._recordingId = null;
    this._recognitionId = null;
    this.emitter = ee({});
  }

  addEventListener(name, handler) {
    this.emitter.on(name, handler);
  }

  removeEventListener(name, handler) {
    this.emitter.off(name, handler);
  }

  fireEvent(name, args = []) {
    this.emitter.emit(name, ...args);
  }

  /**
   * Assemble a HTTP Authentication header.
   */
  _getAuthHeaders() {
    if (!this.settings.oAuth2Token) {
      return Promise.reject('Please set oAuth2Token');
    }
    const authHeader = 'Bearer ' + this.settings.oAuth2Token;
    return Promise.resolve(authHeader);
  }

  /**
   * Create a connection to the websocket server.
   *
   */
  webSocketConnect(accessToken) {
    /**
     * This callback is fired during Ticket-based authentication
     *
     */
    function onOAuth2Challenge(session, method) {
      if (method === 'ticket') {
        return accessToken;
      }
      throw new Error(`don't know how to authenticate using '${method}'`);
    }

    const authUrl = this.settings.wsUrl;
    let connection = null;
    // Open a websocket connection for streaming audio
    try {
      // Set up WAMP connection to router
      connection = new autobahn.Connection({
        url: authUrl,
        realm: 'default',
        // the following attributes must be set for Ticket-based authentication
        authmethods: ['ticket'],
        authid: 'oauth2',
        details: {
          ticket: accessToken
        },
        onchallenge: onOAuth2Challenge
      });
    } catch (e) {
      console.log('WebSocket creation error: ' + e);
      return;
    }
    const self = this;
    connection.onerror = function(e) {
      console.log('WebSocket error: ' + e);
      self.fireEvent('websocketError', [e]);
    };
    connection.onopen = function(session) {
      console.log('WebSocket connection opened');
      self._session = session;
      const _call = self._session.call;
      self._session.call = function(url, ...args) {
        console.debug('Calling RPC: ' + url);
        return _call.call(this, url, ...args);
      };
      self.fireEvent('websocketOpened');
    };
    connection.onclose = function() {
      console.log('WebSocket disconnected');
      self._session = null;
      self.fireEvent('websocketClosed');
    };
    connection.open();
  }

  /**
   * Perform a HTTP GET to the API using authentication.
   *
   * @param {string} URL to retrieve.
   * @returns Promise containing a result.
   */
  _secureAjaxGet(url) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        if (typeof auth !== 'undefined') {
          headers.append('Authorization', auth);
        }
        const options = {
          method: 'GET',
          headers
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Perform a HTTP POST to the API using authentication.
   *
   * @param {string} URL to submit to.
   * @param {FormData} formdata The form to POST.
   * @returns Promise containing a result.
   */
  _secureAjaxPost(url, formdata) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        if (typeof auth !== 'undefined') {
          headers.append('Authorization', auth);
        }
        if (typeof formdata === 'string') {
          headers.append('Content-Type',
            'application/json; charset=utf-8');
        }
        const options = {
          method: 'POST',
          headers,
          body: formdata
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Perform a HTTP DELETE to the API using authentication.
   *
   * @param {string} URL to submit to.
   * @returns Promise containing a result.
   */
  _secureAjaxDelete(url) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        if (typeof auth !== 'undefined') {
          headers.append('Authorization', auth);
        }
        const options = {
          method: 'DELETE',
          headers
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Add an access token to the given URL.
   *
   * @param {string} url The URL to add an access token to.
   */
  addAccessToken(url) {
    if (!this.settings.oAuth2Token) {
      throw new Error('Please set oAuth2Token');
    }
    const secureUrl = url + (url.match(/\?/) ? '&' : '?') + 'access_token=' +
      encodeURIComponent(this.settings.oAuth2Token);
    return secureUrl;
  }

  /**
   * Logs browser compatibility for required and optional SDK capabilities.
   * In case of compatibility issues, an error is thrown.
   */
  static _sdkCompatibility() {
    // WebSocket
    // http://caniuse.com/#feat=websockets
    const canCreateWebSocket = 'WebSocket' in window;
    console.log('Native WebSocket capability: ' +
      canCreateWebSocket);

    if (!canCreateWebSocket) {
      throw new Error('No WebSocket capabilities');
    }
  }

  /**
   * Cancel any current streaming audio recording.
   *
   * @param {its.AudioRecorder} recorder The audio recorder currently recording.
   */
  cancelStreaming(recorder) {
    const self = this;

    if (this._recordingId === null && this._analysisId === null && this._recognitionId === null) {
      console.info('No session in progress, nothing to cancel.');
      return;
    }

    recorder.removeAllEventListeners();
    if (recorder.isRecording()) {
      recorder.stop();
    }

    // This session is over.
    self._recordingId = null;
    self._analysisId = null;
    self._recognitionId = null;
  }

  /**
   * Log a RPC error to the console.
   *
   * @param {object} result Autobahn error object.
   */
  static logRPCError(result) {
    console.error('RPC error returned:', result.error);
  }

  /**
   * Ask the server for an OAuth2 token.
   * @param {BasicAuth} basicAuth Basic Auth to obtain credentials from.
   * @param organisationId Id of the organisation to request a token for.
   * @param studentId Id of the student to request a token for.
   * @returns {Promise} Promise containing a access_token, token_type and scope.
   * @rejects If the server returned an error.
   */
  getOauth2Token(basicAuth, organisationId, studentId) {
    const url = this.settings.apiUrl + '/tokens';
    let scopes = 'tenant/' + basicAuth.tenantId;
    if (organisationId) {
      scopes += '/organisation/' + organisationId;
    }
    if (organisationId && studentId) {
      scopes += '/student/' + studentId;
    }
    const headers = new Headers();
    headers.append('Content-Type',
      'application/x-www-form-urlencoded; charset=utf8');
    const formData = 'grant_type=password&scope=' + scopes +
      '&username=' + basicAuth.principal +
      '&password=' + basicAuth.credentials;
    const options = {
      method: 'POST',
      headers,
      body: formData
    };
    return fetch(url, options)
      .then(response =>
        response.json()
          .then(data => {
            if (response.ok) {
              return data;
            }
            throw data;
          })
      );
  }
};
<commit_msg>refactor(connection): remove header check<commit_after>/* eslint-disable
camelcase
 */

const autobahn = require('autobahn');
const ee = require('event-emitter');
/**
 * Controller class for managing connection interaction.
 */
module.exports = class Connection {

  constructor(options) {
    this.settings = Object.assign({
      // ITSL connection parameters.
      apiUrl: 'https://api.itslanguage.nl',
      oAuth2Token: null,
      wsUrl: null,
      wsToken: null
    }, options);
    Connection._sdkCompatibility();
    this._analysisId = null;
    this._recordingId = null;
    this._recognitionId = null;
    this.emitter = ee({});
  }

  addEventListener(name, handler) {
    this.emitter.on(name, handler);
  }

  removeEventListener(name, handler) {
    this.emitter.off(name, handler);
  }

  fireEvent(name, args = []) {
    this.emitter.emit(name, ...args);
  }

  /**
   * Assemble a HTTP Authentication header.
   */
  _getAuthHeaders() {
    if (!this.settings.oAuth2Token) {
      return Promise.reject('Please set oAuth2Token');
    }
    const authHeader = 'Bearer ' + this.settings.oAuth2Token;
    return Promise.resolve(authHeader);
  }

  /**
   * Create a connection to the websocket server.
   *
   */
  webSocketConnect(accessToken) {
    /**
     * This callback is fired during Ticket-based authentication
     *
     */
    function onOAuth2Challenge(session, method) {
      if (method === 'ticket') {
        return accessToken;
      }
      throw new Error(`don't know how to authenticate using '${method}'`);
    }

    const authUrl = this.settings.wsUrl;
    let connection = null;
    // Open a websocket connection for streaming audio
    try {
      // Set up WAMP connection to router
      connection = new autobahn.Connection({
        url: authUrl,
        realm: 'default',
        // the following attributes must be set for Ticket-based authentication
        authmethods: ['ticket'],
        authid: 'oauth2',
        details: {
          ticket: accessToken
        },
        onchallenge: onOAuth2Challenge
      });
    } catch (e) {
      console.log('WebSocket creation error: ' + e);
      return;
    }
    const self = this;
    connection.onerror = function(e) {
      console.log('WebSocket error: ' + e);
      self.fireEvent('websocketError', [e]);
    };
    connection.onopen = function(session) {
      console.log('WebSocket connection opened');
      self._session = session;
      const _call = self._session.call;
      self._session.call = function(url, ...args) {
        console.debug('Calling RPC: ' + url);
        return _call.call(this, url, ...args);
      };
      self.fireEvent('websocketOpened');
    };
    connection.onclose = function() {
      console.log('WebSocket disconnected');
      self._session = null;
      self.fireEvent('websocketClosed');
    };
    connection.open();
  }

  /**
   * Perform a HTTP GET to the API using authentication.
   *
   * @param {string} URL to retrieve.
   * @returns Promise containing a result.
   */
  _secureAjaxGet(url) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        headers.append('Authorization', auth);
        const options = {
          method: 'GET',
          headers
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Perform a HTTP POST to the API using authentication.
   *
   * @param {string} URL to submit to.
   * @param {FormData} formdata The form to POST.
   * @returns Promise containing a result.
   */
  _secureAjaxPost(url, formdata) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        headers.append('Authorization', auth);
        if (typeof formdata === 'string') {
          headers.append('Content-Type',
            'application/json; charset=utf-8');
        }
        const options = {
          method: 'POST',
          headers,
          body: formdata
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Perform a HTTP DELETE to the API using authentication.
   *
   * @param {string} URL to submit to.
   * @returns Promise containing a result.
   */
  _secureAjaxDelete(url) {
    return this._getAuthHeaders()
      .then(auth => {
        const headers = new Headers();
        headers.append('Authorization', auth);
        const options = {
          method: 'DELETE',
          headers
        };
        return fetch(url, options)
          .then(response =>
            response.text()
              .then(textResponse => {
                if (!textResponse) {
                  return Promise.reject(response.status + ': ' + response.statusText);
                }
                const result = JSON.parse(textResponse);
                if (response.ok) {
                  return result;
                }
                return Promise.reject(result);
              })
          );
      });
  }

  /**
   * Add an access token to the given URL.
   *
   * @param {string} url The URL to add an access token to.
   */
  addAccessToken(url) {
    if (!this.settings.oAuth2Token) {
      throw new Error('Please set oAuth2Token');
    }
    const secureUrl = url + (url.match(/\?/) ? '&' : '?') + 'access_token=' +
      encodeURIComponent(this.settings.oAuth2Token);
    return secureUrl;
  }

  /**
   * Logs browser compatibility for required and optional SDK capabilities.
   * In case of compatibility issues, an error is thrown.
   */
  static _sdkCompatibility() {
    // WebSocket
    // http://caniuse.com/#feat=websockets
    const canCreateWebSocket = 'WebSocket' in window;
    console.log('Native WebSocket capability: ' +
      canCreateWebSocket);

    if (!canCreateWebSocket) {
      throw new Error('No WebSocket capabilities');
    }
  }

  /**
   * Cancel any current streaming audio recording.
   *
   * @param {its.AudioRecorder} recorder The audio recorder currently recording.
   */
  cancelStreaming(recorder) {
    const self = this;

    if (this._recordingId === null && this._analysisId === null && this._recognitionId === null) {
      console.info('No session in progress, nothing to cancel.');
      return;
    }

    recorder.removeAllEventListeners();
    if (recorder.isRecording()) {
      recorder.stop();
    }

    // This session is over.
    self._recordingId = null;
    self._analysisId = null;
    self._recognitionId = null;
  }

  /**
   * Log a RPC error to the console.
   *
   * @param {object} result Autobahn error object.
   */
  static logRPCError(result) {
    console.error('RPC error returned:', result.error);
  }

  /**
   * Ask the server for an OAuth2 token.
   * @param {BasicAuth} basicAuth Basic Auth to obtain credentials from.
   * @param organisationId Id of the organisation to request a token for.
   * @param studentId Id of the student to request a token for.
   * @returns {Promise} Promise containing a access_token, token_type and scope.
   * @rejects If the server returned an error.
   */
  getOauth2Token(basicAuth, organisationId, studentId) {
    const url = this.settings.apiUrl + '/tokens';
    let scopes = 'tenant/' + basicAuth.tenantId;
    if (organisationId) {
      scopes += '/organisation/' + organisationId;
    }
    if (organisationId && studentId) {
      scopes += '/student/' + studentId;
    }
    const headers = new Headers();
    headers.append('Content-Type',
      'application/x-www-form-urlencoded; charset=utf8');
    const formData = 'grant_type=password&scope=' + scopes +
      '&username=' + basicAuth.principal +
      '&password=' + basicAuth.credentials;
    const options = {
      method: 'POST',
      headers,
      body: formData
    };
    return fetch(url, options)
      .then(response =>
        response.json()
          .then(data => {
            if (response.ok) {
              return data;
            }
            throw data;
          })
      );
  }
};
<|endoftext|>


<commit_before>define(['text!./analyzer-question.html', 'app', 'lodash', 'ngSanitize'], function(templateHtml, app, _) {

    var GREEN = { R:167, G:201, B: 27 };
    var BLUE  = { R: 70, G:163, B:230 };
    var WHITE = { R:255, G:255, B:255 };

    //==============================================
    //
    //
    //==============================================
    app.directive('nationalReportAnalyzerQuestion', [function() {
        return {
            restrict : 'E',
            replace : true,
            template : templateHtml,
            require : '^nationalReportAnalyzer',
            scope :  {
                question : '=',
                reports : '=',
                regions : '=',
                sumType : '='
            },
            link: function ($scope, el, attr, nrAnalyzer) {

                var analyzed = false;

                $scope.$watch('::question', function() { if(!analyzed) analyze(); });
                $scope.$watch('::reports',  function() { if(!analyzed) analyze(); });
                $scope.$watch('::regions',  function() { if(!analyzed) analyze(); });
                $scope.$on('nrAnalyzer.nrFilter', analyze);

                //==============================================
                //
                //
                //==============================================
                $scope.showSumTypeSelector = function() {
                    nrAnalyzer.showSumTypeSelector(true);
                };

                //==============================================
                //
                //
                //==============================================
                $scope.showTexts = function(governments) {

                    if(!governments)
                        governments = _.pluck($scope.reports, 'government');

                    var results = _($scope.reports).filter(function(report) {

                        return governments.indexOf(report.government)>=0;

                    }).filter(function(report) {

                        return !_.isEmpty(report[$scope.question.key]);

                    }).map(function(report) {

                        return {
                            government : report.government,
                            text : report[$scope.question.key]
                        };

                    }).value();

                    nrAnalyzer.showTexts(results, $scope.question);
                };

                //==============================================
                //
                //
                //==============================================
                $scope.toggleFilter = function(option) {

                    var question = $scope.question;
                    var oldFilter = nrAnalyzer.filter();

                    if(!option || (oldFilter && oldFilter.question.key == question.key && oldFilter.option.value == option.value)) {
                        nrAnalyzer.filter(undefined);
                        return;
                    }

                    var countriesMap = _($scope.reports).filter(function(report) {

                        var answer = report[question.key];

                        return answer && (answer == option.value || answer.indexOf(option.value)>=0);

                    }).reduce(function(ret, report){

                        ret[report.government] = report.government;
                        return ret;

                    }, {});

                    nrAnalyzer.filter({
                        question : question,
                        option : option,
                        matchingCountriesMap : countriesMap
                    });
                };

                //==============================================
                //
                //
                //==============================================
                function analyze() {

                    if(!$scope.reports) return;
                    if(!$scope.regions) return;
                    if(!$scope.question) return;

                    analyzed = true;

                    console.time($scope.question.key);

                    $scope.reportsMap = {};
                    $scope.filter     = nrAnalyzer.filter();

                    var filter = $scope.filter;
                    var reports = $scope.reports;
                    var regions  = $scope.regions;
                    var question = $scope.question;
                    var options  = $scope.question.options;
                    var reportsMap = $scope.reportsMap;
                    var optionsMap = {};

                    if(question.type=='text') {
                        options = [{ value: 'text' }];  // text responses don't have predefine values; Simulate a fake one
                        $scope.question.options = options;
                    }

                    options.forEach(function(option, i) {

                        optionsMap[option.value] = option;

                        option.sum = 0;
                        option.percent = "0%";
                        option.regions = {};
                        option.color = question.type=='option' ? blendColor(GREEN, BLUE, i/(options.length-1)) : BLUE;
                        option.htmlColor = htmlColor(option.color);
                        option.filterOn  = filter && filter.question.key == question.key && filter.option.value == option.value;
                        option.regions   = _(regions).reduce(function(ret, region){
                            ret[region.identifier] = {
                                sum : 0,
                                percentRow: "0%",
                                percentColumn: "0%",
                                percentGlobal: "0%",
                                htmlColor: htmlColor(WHITE)
                            };
                            return ret;
                        }, {});
                    });

                    question.sum = 0;
                    question.regions = _(regions).reduce(function(ret, region){
                        ret[region.identifier] = { sum : 0 };
                        return ret;
                    }, {});

                    reports.forEach(function(report) {

                        reportsMap[report.government] = report;

                        var excluded = filter && !filter.matchingCountriesMap[report.government];
                        var answers = report[question.key];

                        if(_.isEmpty(answers))
                            answers = undefined;

                        if(question.type=='text' && !!answers) {
                            answers = 'text';
                        }

                        answers = _([answers]).flatten().compact().value();

                        answers.forEach(function(value){

                            var option = optionsMap[value.toString()];

                            if(!option) {
                                return;
                            }

                            if(!excluded) {
                                question.sum++;
                                option  .sum++;
                                option.percent = Math.round((option.sum*100) / question.sum).toString() + "%";
                            }

                            regions.forEach(function(region){

                                var qRegion = question.regions[region.identifier];
                                var oRegion = option  .regions[region.identifier];

                                if(!excluded && region.countriesMap[report.government]) {
                                    qRegion.sum++; // column
                                    oRegion.sum++; // column

                                    var percentRow = Math.round((oRegion.sum*100) / option.sum  );

                                    oRegion.percentRow    = percentRow + "%";
                                    oRegion.percentColumn = Math.round((oRegion.sum*100) / qRegion.sum ).toString() + "%";
                                    oRegion.percentGlobal = Math.round((oRegion.sum*100) / question.sum).toString() + "%";
                                    oRegion.htmlColor     = htmlColor(blendColor(WHITE, option.color, percentRow/100));
                                }
                            });
                        });
                    });

                    console.timeEnd($scope.question.key);

                }

                //============================================================
                //
                //
                //============================================================
                function blendColor(colorA, colorB, ratio) {
                    return {
                        R : colorA.R + Math.floor((colorB.R-colorA.R) * ratio),
                        G : colorA.G + Math.floor((colorB.G-colorA.G) * ratio),
                        B : colorA.B + Math.floor((colorB.B-colorA.B) * ratio)
                    };
                }

                //============================================================
                //
                //
                //============================================================
                function htmlColor(color) {

                    var RR = color.R.toString(16);
                    var GG = color.G.toString(16);
                    var BB = color.B.toString(16);

                    if(RR.length<2) RR = '0'+RR;
                    if(GG.length<2) GG = '0'+GG;
                    if(BB.length<2) BB = '0'+BB;

                    return '#'+RR+GG+BB;
                }

            }
        };
    }]);
});
<commit_msg>Fix number calculation when filtered<commit_after>define(['text!./analyzer-question.html', 'app', 'lodash', 'ngSanitize'], function(templateHtml, app, _) {

    var GREEN = { R:167, G:201, B: 27 };
    var BLUE  = { R: 70, G:163, B:230 };
    var WHITE = { R:255, G:255, B:255 };

    //==============================================
    //
    //
    //==============================================
    app.directive('nationalReportAnalyzerQuestion', [function() {
        return {
            restrict : 'E',
            replace : true,
            template : templateHtml,
            require : '^nationalReportAnalyzer',
            scope :  {
                question : '=',
                reports : '=',
                regions : '=',
                sumType : '='
            },
            link: function ($scope, el, attr, nrAnalyzer) {

                var analyzed = false;

                $scope.$watch('::question', function() { if(!analyzed) analyze(); });
                $scope.$watch('::reports',  function() { if(!analyzed) analyze(); });
                $scope.$watch('::regions',  function() { if(!analyzed) analyze(); });
                $scope.$on('nrAnalyzer.nrFilter', analyze);

                //==============================================
                //
                //
                //==============================================
                $scope.showSumTypeSelector = function() {
                    nrAnalyzer.showSumTypeSelector(true);
                };

                //==============================================
                //
                //
                //==============================================
                $scope.showTexts = function(governments) {

                    if(!governments)
                        governments = _.pluck($scope.reports, 'government');

                    var results = _($scope.reports).filter(function(report) {

                        return governments.indexOf(report.government)>=0;

                    }).filter(function(report) {

                        return !_.isEmpty(report[$scope.question.key]);

                    }).map(function(report) {

                        return {
                            government : report.government,
                            text : report[$scope.question.key]
                        };

                    }).value();

                    nrAnalyzer.showTexts(results, $scope.question);
                };

                //==============================================
                //
                //
                //==============================================
                $scope.toggleFilter = function(option) {

                    var question = $scope.question;
                    var oldFilter = nrAnalyzer.filter();

                    if(!option || (oldFilter && oldFilter.question.key == question.key && oldFilter.option.value == option.value)) {
                        nrAnalyzer.filter(undefined);
                        return;
                    }

                    var countriesMap = _($scope.reports).filter(function(report) {

                        var answer = report[question.key];

                        return answer && (answer == option.value || answer.indexOf(option.value)>=0);

                    }).reduce(function(ret, report){

                        ret[report.government] = report.government;
                        return ret;

                    }, {});

                    nrAnalyzer.filter({
                        question : question,
                        option : option,
                        matchingCountriesMap : countriesMap
                    });
                };

                //==============================================
                //
                //
                //==============================================
                function analyze() {

                    if(!$scope.reports) return;
                    if(!$scope.regions) return;
                    if(!$scope.question) return;

                    analyzed = true;

                    console.time($scope.question.key);

                    $scope.reportsMap = {};
                    $scope.filter     = nrAnalyzer.filter();

                    var filter = $scope.filter;
                    var reports = $scope.reports;
                    var regions  = $scope.regions;
                    var question = $scope.question;
                    var options  = $scope.question.options;
                    var reportsMap = $scope.reportsMap;
                    var optionsMap = {};

                    if(question.type=='text') {
                        options = [{ value: 'text' }];  // text responses don't have predefine values; Simulate a fake one
                        $scope.question.options = options;
                    }

                    options.forEach(function(option, i) {

                        optionsMap[option.value] = option;

                        option.sum = 0;
                        option.fullSum = 0;
                        option.percent = "0%";
                        option.regions = {};
                        option.color = question.type=='option' ? blendColor(GREEN, BLUE, i/(options.length-1)) : BLUE;
                        option.htmlColor = htmlColor(option.color);
                        option.filterOn  = filter && filter.question.key == question.key && filter.option.value == option.value;
                        option.regions   = _(regions).reduce(function(ret, region){
                            ret[region.identifier] = {
                                sum : 0,
                                percentRow: "0%",
                                percentColumn: "0%",
                                percentGlobal: "0%",
                                htmlColor: htmlColor(WHITE)
                            };
                            return ret;
                        }, {});
                    });

                    question.sum = 0;
                    question.fullSum = 0;
                    question.regions = _(regions).reduce(function(ret, region){
                        ret[region.identifier] = { sum : 0 };
                        return ret;
                    }, {});

                    reports.forEach(function(report) {

                        reportsMap[report.government] = report;

                        var included = !filter || !!filter.matchingCountriesMap[report.government];
                        var answers = report[question.key];

                        if(_.isEmpty(answers))
                            answers = undefined;

                        if(question.type=='text' && !!answers) {
                            answers = 'text';
                        }

                        answers = _([answers]).flatten().compact().value();

                        answers.forEach(function(value){

                            var option = optionsMap[value.toString()];

                            if(!option) {
                                return;
                            }

                            question.fullSum++;
                            option  .fullSum++;

                            if(included) {
                                question.sum++;
                                option  .sum++;
                                option.percent = Math.round((option.sum*100) / question.sum).toString() + "%";
                            }

                            regions.forEach(function(region){

                                var qRegion = question.regions[region.identifier];
                                var oRegion = option  .regions[region.identifier];

                                if(included && region.countriesMap[report.government]) {
                                    qRegion.sum++; // column
                                    oRegion.sum++; // column
                                }

                                var percentRow = option.sum ? Math.round((oRegion.sum*100) / option.sum) : 0;

                                oRegion.percentRow    = option.sum   ? percentRow + "%" : '-';
                                oRegion.percentColumn = qRegion.sum  ? Math.round((oRegion.sum*100) / qRegion.sum ).toString() + "%" : '-';
                                oRegion.percentGlobal = question.sum ? Math.round((oRegion.sum*100) / question.sum).toString() + "%" : '-';
                                oRegion.htmlColor     = htmlColor(blendColor(WHITE, option.color, percentRow/100));

                            });
                        });
                    });

                    console.timeEnd($scope.question.key);

                }

                //============================================================
                //
                //
                //============================================================
                function blendColor(colorA, colorB, ratio) {
                    return {
                        R : colorA.R + Math.floor((colorB.R-colorA.R) * ratio),
                        G : colorA.G + Math.floor((colorB.G-colorA.G) * ratio),
                        B : colorA.B + Math.floor((colorB.B-colorA.B) * ratio)
                    };
                }

                //============================================================
                //
                //
                //============================================================
                function htmlColor(color) {

                    var RR = color.R.toString(16);
                    var GG = color.G.toString(16);
                    var BB = color.B.toString(16);

                    if(RR.length<2) RR = '0'+RR;
                    if(GG.length<2) GG = '0'+GG;
                    if(BB.length<2) BB = '0'+BB;

                    return '#'+RR+GG+BB;
                }

            }
        };
    }]);
});
<|endoftext|>


<commit_before>/**
 * DashboardCtrl
 */

angular.module(PKG.name+'.feature.dashboard').controller('DashboardCtrl',
function ($scope, $state, $alert, $dropdown, myDashboardsModel, Widget) {

  $scope.dashboards = myDashboardsModel.data;

  /**
   * handle tab navigation
   */
  $scope.$watch('dashboards.activeIndex', function (newVal) {
    $state.go($state.current, {tab:newVal});
  });

  $scope.$on('$stateChangeSuccess', function (event, state) {
    var tab = parseInt($state.params.tab, 10) || 0;
    if((tab<0 || tab>=$scope.dashboards.length)) {
      tab = 0;
    }
    $scope.dashboards.activeIndex = tab;
  });

  /**
   * show a dropdown when clicking on the tab of active dashboard
   * @TODO make a directive instead
   */
  $scope.activeTabClick = function (event) {

    var toggle = angular.element(event.target);
    if(!toggle.hasClass('dropdown-toggle')) {
      toggle = toggle.parent();
    }

    if(toggle.parent().hasClass('open')) {
      return;
    }

    var scope = $scope.$new(),
        dd = $dropdown(toggle, {
          template: 'assets/features/dashboard/partials/tab-dd.html',
          animation: 'am-flip-x',
          trigger: 'manual',
          prefixEvent: 'dashboard-tab-dd',
          scope: scope
        });

    dd.$promise.then(function(){
      dd.show();
    });

    scope.$on('dashboard-tab-dd.hide', function () {
      dd.destroy();
    });

  };

  $scope.addDashboard = function () {
    myDashboardsModel.add({
      title: 'new dashboard'
    });

    $state.go($state.current, {tab: 0}, {reload: true});
  };

  $scope.removeDashboard = function () {
    myDashboardsModel.remove($scope.dashboards.activeIndex);
  };

  $scope.renameDashboard = function (newName) {
    myDashboardsModel.current().rename(newName);
  };

  $scope.removeWidget = function (w) {
    myDashboardsModel.current().removeWidget(w);
  };

  $scope.renameWidget = function (w, newName) {
    myDashboardsModel.current().renameWidget(w, newName);
  };

  $scope.dragdrop = {
    dragStart: function (drag) {
      console.log('dragStart', drag.source, drag.dest);
    },
    dragEnd: function (drag) {
      console.log('dragEnd', drag.source, drag.dest);
      myDashboardsModel.current().persist();
    }
  };

});

<commit_msg>add timestamp to new dashboard name for demo purposes<commit_after>/**
 * DashboardCtrl
 */

angular.module(PKG.name+'.feature.dashboard').controller('DashboardCtrl',
function ($scope, $state, $alert, $dropdown, myDashboardsModel, Widget) {

  $scope.dashboards = myDashboardsModel.data;

  /**
   * handle tab navigation
   */
  $scope.$watch('dashboards.activeIndex', function (newVal) {
    $state.go($state.current, {tab:newVal});
  });

  $scope.$on('$stateChangeSuccess', function (event, state) {
    var tab = parseInt($state.params.tab, 10) || 0;
    if((tab<0 || tab>=$scope.dashboards.length)) {
      tab = 0;
    }
    $scope.dashboards.activeIndex = tab;
  });

  /**
   * show a dropdown when clicking on the tab of active dashboard
   * @TODO make a directive instead
   */
  $scope.activeTabClick = function (event) {

    var toggle = angular.element(event.target);
    if(!toggle.hasClass('dropdown-toggle')) {
      toggle = toggle.parent();
    }

    if(toggle.parent().hasClass('open')) {
      return;
    }

    var scope = $scope.$new(),
        dd = $dropdown(toggle, {
          template: 'assets/features/dashboard/partials/tab-dd.html',
          animation: 'am-flip-x',
          trigger: 'manual',
          prefixEvent: 'dashboard-tab-dd',
          scope: scope
        });

    dd.$promise.then(function(){
      dd.show();
    });

    scope.$on('dashboard-tab-dd.hide', function () {
      dd.destroy();
    });

  };

  $scope.addDashboard = function () {
    myDashboardsModel.add({
      title: 'dashboard '+Date.now()
    });

    $state.go($state.current, {tab: 0}, {reload: true});
  };

  $scope.removeDashboard = function () {
    myDashboardsModel.remove($scope.dashboards.activeIndex);
  };

  $scope.renameDashboard = function (newName) {
    myDashboardsModel.current().rename(newName);
  };

  $scope.removeWidget = function (w) {
    myDashboardsModel.current().removeWidget(w);
  };

  $scope.renameWidget = function (w, newName) {
    myDashboardsModel.current().renameWidget(w, newName);
  };

  $scope.dragdrop = {
    dragStart: function (drag) {
      console.log('dragStart', drag.source, drag.dest);
    },
    dragEnd: function (drag) {
      console.log('dragEnd', drag.source, drag.dest);
      myDashboardsModel.current().persist();
    }
  };

});

<|endoftext|>


<commit_before>import { merge } from '@ember/polyfills';
import configuration from "ember-form-builder/configuration";
import ENV from "../config/environment";
import { classify } from '@ember/string';

import FormBuilder from 'ember-form-builder/models/form-builder';
import EmberValidations from 'ember-form-builder/mixins/ember-validations';
import EmberCpValidations from 'ember-form-builder/mixins/ember-cp-validations';
import EmberData from 'ember-form-builder/mixins/ember-data';
import EmberOrbit from 'ember-form-builder/mixins/ember-orbit';

const ValidationsMixins = {
  EmberValidations: EmberValidations,
  EmberCpValidations: EmberCpValidations
};
const DataMixins = {
  EmberData: EmberData,
  EmberOrbit: EmberOrbit
};

export var initialize = function(/* app */) {
  ENV.formBuilder = merge(configuration, ENV.formBuilder || {});

  let validationsMixin = ValidationsMixins[classify(ENV.formBuilder.validationsAddon)];
  if (validationsMixin) {
    FormBuilder.reopen(validationsMixin);
  }

  let dataMixin = DataMixins[classify(ENV.formBuilder.dataAddon)];
  if (dataMixin) {
    FormBuilder.reopen(dataMixin);
  }
};

export default {
  name: "ember-form-builder-configuration",
  initialize: initialize
};
<commit_msg>removed deprecated merge method<commit_after>import { assign } from '@ember/polyfills';
import configuration from "ember-form-builder/configuration";
import ENV from "../config/environment";
import { classify } from '@ember/string';

import FormBuilder from 'ember-form-builder/models/form-builder';
import EmberValidations from 'ember-form-builder/mixins/ember-validations';
import EmberCpValidations from 'ember-form-builder/mixins/ember-cp-validations';
import EmberData from 'ember-form-builder/mixins/ember-data';
import EmberOrbit from 'ember-form-builder/mixins/ember-orbit';

const ValidationsMixins = {
  EmberValidations: EmberValidations,
  EmberCpValidations: EmberCpValidations
};
const DataMixins = {
  EmberData: EmberData,
  EmberOrbit: EmberOrbit
};

export var initialize = function(/* app */) {
  ENV.formBuilder = assign(configuration, ENV.formBuilder || {});

  let validationsMixin = ValidationsMixins[classify(ENV.formBuilder.validationsAddon)];
  if (validationsMixin) {
    FormBuilder.reopen(validationsMixin);
  }

  let dataMixin = DataMixins[classify(ENV.formBuilder.dataAddon)];
  if (dataMixin) {
    FormBuilder.reopen(dataMixin);
  }
};

export default {
  name: "ember-form-builder-configuration",
  initialize: initialize
};
<|endoftext|>


<commit_before>CodeMirror.defineMode("sparql", function(config) {
  var indentUnit = config.indentUnit;
  var curPunc;

  function wordRegexp(words) {
    return new RegExp("^(?:" + words.join("|") + ")$", "i");
  }
  var ops = wordRegexp(["str", "lang", "langmatches", "datatype", "bound", "sameterm", "isiri", "isuri",
                        "isblank", "isliteral", "union", "a"]);
  var keywords = wordRegexp(["base", "prefix", "select", "distinct", "reduced", "construct", "describe",
                             "ask", "from", "named", "where", "order", "limit", "offset", "filter", "optional",
                             "graph", "by", "asc", "desc"]);
  var operatorChars = /[*+\-<>=&|]/;

  function tokenBase(stream, state) {
    var ch = stream.next();
    curPunc = null;
    if (ch == "$" || ch == "?") {
      stream.match(/^[\w\d]*/);
      return "variable-2";
    }
    else if (ch == "<" && !stream.match(/^[\s\u00a0=]/, false)) {
      stream.match(/^[^\s\u00a0>]*>?/);
      return "atom";
    }
    else if (ch == "\"" || ch == "'") {
      state.tokenize = tokenLiteral(ch);
      return state.tokenize(stream, state);
    }
    else if (/[{}\(\),\.;\[\]]/.test(ch)) {
      curPunc = ch;
      return null;
    }
    else if (ch == "#") {
      stream.skipToEnd();
      return "comment";
    }
    else if (operatorChars.test(ch)) {
      stream.eatWhile(operatorChars);
      return null;
    }
    else if (ch == ":") {
      stream.eatWhile(/[\w\d\._\-]/);
      return "atom";
    }
    else {
      stream.eatWhile(/[_\w\d]/);
      if (stream.eat(":")) {
        stream.eatWhile(/[\w\d_\-]/);
        return "atom";
      }
      var word = stream.current(), type;
      if (ops.test(word))
        return null;
      else if (keywords.test(word))
        return "keyword";
      else
        return "variable";
    }
  }

  function tokenLiteral(quote) {
    return function(stream, state) {
      var escaped = false, ch;
      while ((ch = stream.next()) != null) {
        if (ch == quote && !escaped) {
          state.tokenize = tokenBase;
          break;
        }
        escaped = !escaped && ch == "\\";
      }
      return "string";
    };
  }

  function pushContext(state, type, col) {
    state.context = {prev: state.context, indent: state.indent, col: col, type: type};
  }
  function popContext(state) {
    state.indent = state.context.indent;
    state.context = state.context.prev;
  }

  return {
    startState: function(base) {
      return {tokenize: tokenBase,
              context: null,
              indent: 0,
              col: 0};
    },

    token: function(stream, state) {
      if (stream.sol()) {
        if (state.context && state.context.align == null) state.context.align = false;
        state.indent = stream.indentation();
      }
      if (stream.eatSpace()) return null;
      var style = state.tokenize(stream, state);

      if (style != "comment" && state.context && state.context.align == null && state.context.type != "pattern") {
        state.context.align = true;
      }

      if (curPunc == "(") pushContext(state, ")", stream.column());
      else if (curPunc == "[") pushContext(state, "]", stream.column());
      else if (curPunc == "{") pushContext(state, "}", stream.column());
      else if (/[\]\}\)]/.test(curPunc)) {
        while (state.context && state.context.type == "pattern") popContext(state);
        if (state.context && curPunc == state.context.type) popContext(state);
      }
      else if (curPunc == "." && state.context && state.context.type == "pattern") popContext(state);
      else if (/atom|string|variable/.test(style) && state.context) {
        if (/[\}\]]/.test(state.context.type))
          pushContext(state, "pattern", stream.column());
        else if (state.context.type == "pattern" && !state.context.align) {
          state.context.align = true;
          state.context.col = stream.column();
        }
      }

      return style;
    },

    indent: function(state, textAfter) {
      var firstChar = textAfter && textAfter.charAt(0);
      var context = state.context;
      if (/[\]\}]/.test(firstChar))
        while (context && context.type == "pattern") context = context.prev;

      var closing = context && firstChar == context.type;
      if (!context)
        return 0;
      else if (context.type == "pattern")
        return context.col;
      else if (context.align)
        return context.col + (closing ? 0 : 1);
      else
        return context.indent + (closing ? 0 : indentUnit);
    }
  };
});

CodeMirror.defineMIME("application/x-sparql-query", "sparql");
<commit_msg>Added GROUP and AS to list of SPARQL keywords<commit_after>CodeMirror.defineMode("sparql", function(config) {
  var indentUnit = config.indentUnit;
  var curPunc;

  function wordRegexp(words) {
    return new RegExp("^(?:" + words.join("|") + ")$", "i");
  }
  var ops = wordRegexp(["str", "lang", "langmatches", "datatype", "bound", "sameterm", "isiri", "isuri",
                        "isblank", "isliteral", "union", "a"]);
  var keywords = wordRegexp(["base", "prefix", "select", "distinct", "reduced", "construct", "describe",
                             "ask", "from", "named", "where", "order", "limit", "offset", "filter", "optional",
                             "graph", "by", "asc", "desc", "group", "as"]);
  var operatorChars = /[*+\-<>=&|]/;

  function tokenBase(stream, state) {
    var ch = stream.next();
    curPunc = null;
    if (ch == "$" || ch == "?") {
      stream.match(/^[\w\d]*/);
      return "variable-2";
    }
    else if (ch == "<" && !stream.match(/^[\s\u00a0=]/, false)) {
      stream.match(/^[^\s\u00a0>]*>?/);
      return "atom";
    }
    else if (ch == "\"" || ch == "'") {
      state.tokenize = tokenLiteral(ch);
      return state.tokenize(stream, state);
    }
    else if (/[{}\(\),\.;\[\]]/.test(ch)) {
      curPunc = ch;
      return null;
    }
    else if (ch == "#") {
      stream.skipToEnd();
      return "comment";
    }
    else if (operatorChars.test(ch)) {
      stream.eatWhile(operatorChars);
      return null;
    }
    else if (ch == ":") {
      stream.eatWhile(/[\w\d\._\-]/);
      return "atom";
    }
    else {
      stream.eatWhile(/[_\w\d]/);
      if (stream.eat(":")) {
        stream.eatWhile(/[\w\d_\-]/);
        return "atom";
      }
      var word = stream.current(), type;
      if (ops.test(word))
        return null;
      else if (keywords.test(word))
        return "keyword";
      else
        return "variable";
    }
  }

  function tokenLiteral(quote) {
    return function(stream, state) {
      var escaped = false, ch;
      while ((ch = stream.next()) != null) {
        if (ch == quote && !escaped) {
          state.tokenize = tokenBase;
          break;
        }
        escaped = !escaped && ch == "\\";
      }
      return "string";
    };
  }

  function pushContext(state, type, col) {
    state.context = {prev: state.context, indent: state.indent, col: col, type: type};
  }
  function popContext(state) {
    state.indent = state.context.indent;
    state.context = state.context.prev;
  }

  return {
    startState: function(base) {
      return {tokenize: tokenBase,
              context: null,
              indent: 0,
              col: 0};
    },

    token: function(stream, state) {
      if (stream.sol()) {
        if (state.context && state.context.align == null) state.context.align = false;
        state.indent = stream.indentation();
      }
      if (stream.eatSpace()) return null;
      var style = state.tokenize(stream, state);

      if (style != "comment" && state.context && state.context.align == null && state.context.type != "pattern") {
        state.context.align = true;
      }

      if (curPunc == "(") pushContext(state, ")", stream.column());
      else if (curPunc == "[") pushContext(state, "]", stream.column());
      else if (curPunc == "{") pushContext(state, "}", stream.column());
      else if (/[\]\}\)]/.test(curPunc)) {
        while (state.context && state.context.type == "pattern") popContext(state);
        if (state.context && curPunc == state.context.type) popContext(state);
      }
      else if (curPunc == "." && state.context && state.context.type == "pattern") popContext(state);
      else if (/atom|string|variable/.test(style) && state.context) {
        if (/[\}\]]/.test(state.context.type))
          pushContext(state, "pattern", stream.column());
        else if (state.context.type == "pattern" && !state.context.align) {
          state.context.align = true;
          state.context.col = stream.column();
        }
      }

      return style;
    },

    indent: function(state, textAfter) {
      var firstChar = textAfter && textAfter.charAt(0);
      var context = state.context;
      if (/[\]\}]/.test(firstChar))
        while (context && context.type == "pattern") context = context.prev;

      var closing = context && firstChar == context.type;
      if (!context)
        return 0;
      else if (context.type == "pattern")
        return context.col;
      else if (context.align)
        return context.col + (closing ? 0 : 1);
      else
        return context.indent + (closing ? 0 : indentUnit);
    }
  };
});

CodeMirror.defineMIME("application/x-sparql-query", "sparql");
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
    /**
     * Iterates over the options in the select element and returns a semicolon-delimited string of the selected values
     */
    getDomElementValue : function (el) {
        var selectedOptions = [];
        for (var i = 0; i < el.options.length; i++) {
            if (el.options[i].selected) {
                selectedOptions.push(el.options[i].value);
            }
        }
        return selectedOptions.join(";");
    },

    /**
     * Returns a package with the array of options (as either an array of components or an array of JS objects)
     * and the strategy to work with that array
     */
    getOptionsWithStrategy: function(cmp) {
        var opts = cmp.get("v.options"),
        	strat = this.optionsStrategy;

        if ($A.util.isEmpty(opts)) {
        	opts = cmp.get("v.body");
        	strat = this.bodyStrategy;
        }

        return { options : opts, strategy : strat };
    },

    /**
     * Updates all options' "selected" attributes in the select element, based on the semicolon-delimited newValue string
     */
    updateOptionsFromValue: function(cmp) {
    	if (cmp._suspendChangeHandlers) {
    		return;
    	}
        var value = cmp.get("v.value"),
        	optionsPack = this.getOptionsWithStrategy(cmp),
        	selectedOptions = optionsPack.strategy.getSelected(optionsPack.options);

        if (optionsPack.options.length == 0) {
        	cmp._initOptionsFromValue = true;
        	return;
        }

        if (selectedOptions.found && value === selectedOptions.optionValue) {
            return;
        }

        var newValues = (cmp.get("v.value") || "").split(";");

        if (!optionsPack.strategy.updateOptions(optionsPack.options, newValues)) {
        	this.updateValueFromOptions(cmp, optionsPack);
        } else {
        	cmp._suspendChangeHandlers = true;
        	optionsPack.strategy.persistOptions(cmp, optionsPack.options);
        	cmp._suspendChangeHandlers = false;
        }
    },

    /**
     * Updates this component's "value" attribute based on the state of its options' "selected" attributes
     */
    updateValueFromOptions: function(cmp, optionsPack) {
        if (cmp._suspendChangeHandlers) {
        	return;
    	}

        var value = cmp.get("v.value"),
        	isMultiple = $A.util.getBooleanValue(cmp.get("v.multiple")),
        	optionsPack = optionsPack || this.getOptionsWithStrategy(cmp),
        	selectedOptions = optionsPack.strategy.getSelected(optionsPack.options);

        if (!selectedOptions.found || value !== selectedOptions.optionValue) {
        	if (!isMultiple && !selectedOptions.found) {
        		selectedOptions.optionValue = optionsPack.strategy.getValue(optionsPack.options, 0);
        		optionsPack.strategy.setOption(optionsPack.options, 0, true);

        		cmp._suspendChangeHandlers = true;
            	optionsPack.strategy.persistOptions(cmp, optionsPack.options);
            	cmp._suspendChangeHandlers = false;
        	}
        	cmp.set("v.value", selectedOptions.optionValue, true);
        }
    },

    /**
     * Strategies for working with either an array of option objects or of body components, passed through to the
     * select component either through the "v.options" attribute or through the body in markup.
     * Abstracts the implementation away so that the logic specific to the two data structures can be separated from
     * the main component logic
     *
     * Main functions available:
     *   updateOptions(options, newValues) - updates the list of options based on newValues, which is either an array or a string
     *   	Used for ensuring consistency between "v.value" and the list of options
     *   getValues(options) - returns a ';'-concatenated String of selected values and whether a selected value was found
     *   	Used for seeing which options are selected from the perspective of the options
     *   getText(options, index) - returns the internal text of options[index]
     *   setOption(options, index, selected) - equivalent to options[index].selected = selected
     *   persistOptions(cmp, options) - persists the array of options into the appropriate component attribute
     */

    /**
     * Strategy object for an array of option objects
     */
    optionsStrategy: {
    	// If an option is in newValues, we want to select it
    	updateOptions : function(options, newValues) {
    		var isMultiple = newValues.length > 1;
    		var updated = false;

    		$A.util.forEach(options, function(option) {
    			var val = option.value;
    			var selectOption = (isMultiple && aura.util.arrayIndexOf(newValues, val) > -1) || newValues[0] == val.toString();

    			updated = updated || (option.selected != selectOption);
    			option.selected = selectOption;
    		}, this);

    		return updated;
    	},
    	// If an option is selected, we want to aggregate it into our list
    	getSelected : function(options) {
    		var values = [];

    		$A.util.forEach(options, function(option) {
    			if (option.selected) {
    				values.push(option.value);
    			}
    		}, this);

    		return { found : (values.length > 0), optionValue : values.join(";") };
    	},
    	getValue : function(options, index) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			return options[index].value;
    		}
    		return undefined;
    	},
    	setOption : function(options, index, selected) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			options[index].selected = selected;
    		} else {
    			// TODO: somehow expose that the option couldn't be set.
    		}
    	},
    	persistOptions : function(cmp, options) {
    		cmp.set("v.options", options);
    	}
    },

    /**]
     * Strategy object for an array of components (used for maintaining support for using inputSelectOption components in the body)
     */
    bodyStrategy: {
    	// Updates options based on their existence in newValues
    	updateOptions : function(options, newValues) {
    		var parameters = { newValues : newValues,
    						   valueIsArray : $A.util.isArray(newValues),
    						   isUndefinedOrNull : $A.util.isUndefinedOrNull(newValues)
    		}

            var result = { updated : false };
            // Perform single option update function on all of our options
    		this.performOperationOnCmps(options, this.updateOption, result, parameters);
    		return result.updated;
    	},
    	getSelected : function(bodyCmps) {
    		var values = [];
    		this.performOperationOnCmps(bodyCmps, this.pushIfSelected, values);
    		return { found : (values.length > 0), optionValue : values.join(";") };
    	},
    	getValue : function(options, index) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			return options[index].get("v.text");
    		}
    		return undefined;
    	},
    	setOption : function(options, index, selected) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			options[index].set("v.value", selected);
    		} else {
    			// TODO: somehow expose that the option couldn't be set.
    		}
    	},
    	persistOptions : function(cmp, options) {
    		cmp.set("v.body", options);
    	},
    	// Performs op on every ui:inputSelectOption in opts, where op = function(optionCmp, resultsObject, optionalArguments)
    	performOperationOnCmps : function(opts, op, result, opParams) {
    		$A.util.forEach(opts, function(cmp) {
        		var descriptor = cmp.getDef().getDescriptor();
        		var cmpName = descriptor.getNamespace() + ":" + descriptor.getName();
        		if (cmpName === "ui:inputSelectOptionGroup") {
        			var groupBody = cmp.get("v.body");
        			if (!$A.util.isEmpty(groupBody)) {
        				$A.util.forEach(groupBody, function(groupBodyCmp) {
        					var descriptor = groupBodyCmp.getDef().getDescriptor();
        					if ((descriptor.getNamespace() + ":" + descriptor.getName()) === "ui:inputSelectOption") {
        						op(groupBodyCmp, result, opParams);
        					}
        				}, this);
        			}
        		} else if (cmpName = "ui:inputSelectOption") {
    				op(cmp, result, opParams);
    			}
        	}, this);
        },
        // Helper function for updateOptions
        // Update optionCmp if it exists in newValues; passes result back in result object
        updateOption : function(optionCmp, result, params) {
            if (!params.isUndefinedOrNull) {
            	if (params.valueIsArray) {
    	            for(var i=0;i<params.newValues.length;i++){
    	                if (params.newValues[i] === optionCmp.get("v.text")) {
    	                    result.updated = true;
    	                    break;
    	                }
    	            }
            	} else if (params.newValues === optionCmp.get("v.text")) {
            		result.updated = true;
            	}
            }
            var originalStatus = $A.util.getBooleanValue(optionCmp.get("v.value"));
            if (originalStatus !== result.updated) {
                optionCmp.set("v.value", result.updated);
            }
        },
        // Helper function for getValues
        // Push optionCmp's value into valueList if selected
		pushIfSelected : function(optionCmp, valueList) {
			if ($A.util.getBooleanValue(optionCmp.get("v.value")) === true) {
				var text = optionCmp.get("v.text");
				if (!$A.util.isUndefined(text)) {
					valueList.push(text);
				}
			}
		}
    },

    /**
     * Render the options directly to the DOM for performance
     */
    renderOptions: function(cmp) {
    	var options = cmp.get("v.options"),
			select = cmp.find("select").getElement(),
			optFrag, option, internalText;

		if ($A.util.isEmpty(options)) {
			return;
		}

    	optFrag = document.createDocumentFragment();
    	for (var i = 0; i < options.length; i++) {
    		option = document.createElement("option");
    		internalText = ($A.util.isEmpty(options[i].label) ? options[i].value : options[i].label);

    		option.label = options[i].label;
    		option.value = options[i].value;
    		option.setAttribute("class", options[i]["class"]);

    		if (options[i].selected) {
    			option.selected = "selected";
    		}

    		if (options[i].disabled) {
    			option.disabled = "disabled";
    		}

    		option.appendChild(document.createTextNode(internalText));

    		optFrag.appendChild(option);
    	}

    	while (select.firstChild) {
    		select.removeChild(select.firstChild);
    	}
    	select.appendChild(optFrag);
    }

})<commit_msg>Fixed bug with option components in inputSelect being selected when they shouldn't be<commit_after>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
    /**
     * Iterates over the options in the select element and returns a semicolon-delimited string of the selected values
     */
    getDomElementValue : function (el) {
        var selectedOptions = [];
        for (var i = 0; i < el.options.length; i++) {
            if (el.options[i].selected) {
                selectedOptions.push(el.options[i].value);
            }
        }
        return selectedOptions.join(";");
    },

    /**
     * Returns a package with the array of options (as either an array of components or an array of JS objects)
     * and the strategy to work with that array
     */
    getOptionsWithStrategy: function(cmp) {
        var opts = cmp.get("v.options"),
        	strat = this.optionsStrategy;

        if ($A.util.isEmpty(opts)) {
        	opts = cmp.get("v.body");
        	strat = this.bodyStrategy;
        }

        return { options : opts, strategy : strat };
    },

    /**
     * Updates all options' "selected" attributes in the select element, based on the semicolon-delimited newValue string
     */
    updateOptionsFromValue: function(cmp) {
    	if (cmp._suspendChangeHandlers) {
    		return;
    	}
        var value = cmp.get("v.value"),
        	optionsPack = this.getOptionsWithStrategy(cmp),
        	selectedOptions = optionsPack.strategy.getSelected(optionsPack.options);

        if (optionsPack.options.length == 0) {
        	cmp._initOptionsFromValue = true;
        	return;
        }

        if (selectedOptions.found && value === selectedOptions.optionValue) {
            return;
        }

        var newValues = (cmp.get("v.value") || "").split(";");

        if (!optionsPack.strategy.updateOptions(optionsPack.options, newValues)) {
        	this.updateValueFromOptions(cmp, optionsPack);
        } else {
        	cmp._suspendChangeHandlers = true;
        	optionsPack.strategy.persistOptions(cmp, optionsPack.options);
        	cmp._suspendChangeHandlers = false;
        }
    },

    /**
     * Updates this component's "value" attribute based on the state of its options' "selected" attributes
     */
    updateValueFromOptions: function(cmp, optionsPack) {
        if (cmp._suspendChangeHandlers) {
        	return;
    	}

        var value = cmp.get("v.value"),
        	isMultiple = $A.util.getBooleanValue(cmp.get("v.multiple")),
        	optionsPack = optionsPack || this.getOptionsWithStrategy(cmp),
        	selectedOptions = optionsPack.strategy.getSelected(optionsPack.options);

        if (!selectedOptions.found || value !== selectedOptions.optionValue) {
        	if (!isMultiple && !selectedOptions.found) {
        		selectedOptions.optionValue = optionsPack.strategy.getValue(optionsPack.options, 0);
        		optionsPack.strategy.setOption(optionsPack.options, 0, true);

        		cmp._suspendChangeHandlers = true;
            	optionsPack.strategy.persistOptions(cmp, optionsPack.options);
            	cmp._suspendChangeHandlers = false;
        	}
        	cmp.set("v.value", selectedOptions.optionValue, true);
        }
    },

    /**
     * Strategies for working with either an array of option objects or of body components, passed through to the
     * select component either through the "v.options" attribute or through the body in markup.
     * Abstracts the implementation away so that the logic specific to the two data structures can be separated from
     * the main component logic
     *
     * Main functions available:
     *   updateOptions(options, newValues) - updates the list of options based on newValues, which is either an array or a string
     *   	Used for ensuring consistency between "v.value" and the list of options
     *   getValues(options) - returns a ';'-concatenated String of selected values and whether a selected value was found
     *   	Used for seeing which options are selected from the perspective of the options
     *   getText(options, index) - returns the internal text of options[index]
     *   setOption(options, index, selected) - equivalent to options[index].selected = selected
     *   persistOptions(cmp, options) - persists the array of options into the appropriate component attribute
     */

    /**
     * Strategy object for an array of option objects
     */
    optionsStrategy: {
    	// If an option is in newValues, we want to select it
    	updateOptions : function(options, newValues) {
    		var updated = false;

    		$A.util.forEach(options, function(option) {
    			var val = option.value;
    			var selectOption = (newValues.length > 1 && aura.util.arrayIndexOf(newValues, val) > -1) || newValues[0] == val.toString();

    			updated = updated || (option.selected != selectOption);
    			option.selected = selectOption;
    		}, this);

    		return updated;
    	},
    	// If an option is selected, we want to aggregate it into our list
    	getSelected : function(options) {
    		var values = [];

    		$A.util.forEach(options, function(option) {
    			if (option.selected) {
    				values.push(option.value);
    			}
    		}, this);

    		return { found : (values.length > 0), optionValue : values.join(";") };
    	},
    	getValue : function(options, index) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			return options[index].value;
    		}
    		return undefined;
    	},
    	setOption : function(options, index, selected) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			options[index].selected = selected;
    		} else {
    			// TODO: somehow expose that the option couldn't be set.
    		}
    	},
    	persistOptions : function(cmp, options) {
    		cmp.set("v.options", options);
    	}
    },

    /**]
     * Strategy object for an array of components (used for maintaining support for using inputSelectOption components in the body)
     */
    bodyStrategy: {
    	// Updates options based on their existence in newValues
    	updateOptions : function(options, newValues) {
            var result = { updated : false };
            // Perform single option update function on all of our options
    		this.performOperationOnCmps(options, this.updateOption, result, newValues);
    		return result.updated;
    	},
    	getSelected : function(bodyCmps) {
    		var values = [];
    		this.performOperationOnCmps(bodyCmps, this.pushIfSelected, values);
    		return { found : (values.length > 0), optionValue : values.join(";") };
    	},
    	getValue : function(options, index) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			return options[index].get("v.text");
    		}
    		return undefined;
    	},
    	setOption : function(options, index, selected) {
    		if (!$A.util.isUndefinedOrNull(options[index])) {
    			options[index].set("v.value", selected);
    		} else {
    			// TODO: somehow expose that the option couldn't be set.
    		}
    	},
    	persistOptions : function(cmp, options) {
    		cmp.set("v.body", options);
    	},
    	// Performs op on every ui:inputSelectOption in opts, where op = function(optionCmp, resultsObject, optionalArguments)
    	performOperationOnCmps : function(opts, op, result, newValues) {
    		$A.util.forEach(opts, function(cmp) {
        		var descriptor = cmp.getDef().getDescriptor();
        		var cmpName = descriptor.getNamespace() + ":" + descriptor.getName();
        		if (cmpName === "ui:inputSelectOptionGroup") {
        			var groupBody = cmp.get("v.body");
        			if (!$A.util.isEmpty(groupBody)) {
        				$A.util.forEach(groupBody, function(groupBodyCmp) {
        					var descriptor = groupBodyCmp.getDef().getDescriptor();
        					if ((descriptor.getNamespace() + ":" + descriptor.getName()) === "ui:inputSelectOption") {
        						op(groupBodyCmp, result, newValues);
        					}
        				}, this);
        			}
        		} else if (cmpName = "ui:inputSelectOption") {
    				op(cmp, result, newValues);
    			}
        	}, this);
        },
        // Helper function for updateOptions
        // Update optionCmp if it exists in newValues; passes result back in result object
        updateOption : function(optionCmp, result, newValues) {
        	var text = optionCmp.get("v.text");
			var selectOption = (newValues.length > 1 && aura.util.arrayIndexOf(newValues, text) > -1) || newValues[0] === text;

			if ($A.util.getBooleanValue(optionCmp.get("v.value")) != selectOption) {
				result.updated = true;
				optionCmp.set("v.value", selectOption);
			}

        },
        // Helper function for getValues
        // Push optionCmp's value into valueList if selected
		pushIfSelected : function(optionCmp, valueList) {
			if ($A.util.getBooleanValue(optionCmp.get("v.value")) === true) {
				var text = optionCmp.get("v.text");
				if (!$A.util.isUndefined(text)) {
					valueList.push(text);
				}
			}
		}
    },

    /**
     * Render the options directly to the DOM for performance
     */
    renderOptions: function(cmp) {
    	var options = cmp.get("v.options"),
			select = cmp.find("select").getElement(),
			optFrag, option, internalText;

		if ($A.util.isEmpty(options)) {
			return;
		}

    	optFrag = document.createDocumentFragment();
    	for (var i = 0; i < options.length; i++) {
    		option = document.createElement("option");
    		internalText = ($A.util.isEmpty(options[i].label) ? options[i].value : options[i].label);

    		option.label = options[i].label;
    		option.value = options[i].value;
    		option.setAttribute("class", options[i]["class"]);

    		if (options[i].selected) {
    			option.selected = "selected";
    		}

    		if (options[i].disabled) {
    			option.disabled = "disabled";
    		}

    		option.appendChild(document.createTextNode(internalText));

    		optFrag.appendChild(option);
    	}

    	while (select.firstChild) {
    		select.removeChild(select.firstChild);
    	}
    	select.appendChild(optFrag);
    }

})<|endoftext|>