<commit_before>import React, { Component } from 'react';
import {AllQuestions} from './questions.js';

class CircleOfFifths extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLastAnswerCorrect: false,
      questionsAvailable: JSON.parse(JSON.stringify(AllQuestions)),
      answer: null,
      value: '',
      questionId: this.selectAQuestionId(AllQuestions.length),
      previousAnswer: '',
      previousQuestion: ''
    };

  }

  selectAQuestionId(amountLeft) {
    amountLeft = Math.max(amountLeft - 1, 0);
    return Math.floor((Math.random() * amountLeft));
  }

  submitAnswer(event) {
    event.preventDefault();
    if(this.state.value !== '' && this.state.questionsAvailable.length > 0) {
      this.setState({answer: this.state.value, value: '', isLastAnswerCorrect: this.evaluateAnswer(this.state.value)});
    }
  }

  nextQuestion(removeQuestion) {
    let questions = JSON.parse(JSON.stringify(this.state.questionsAvailable));
    if(removeQuestion) {
      questions.splice(this.state.questionId, 1);
    }
    let previousAnswer = this.state.questionsAvailable[this.state.questionId].answer[0];
    let previousQuestion = this.state.questionsAvailable[this.state.questionId].question;
    this.setState(
      {
        questionsAvailable: questions,
        questionId: this.selectAQuestionId(questions.length),
        previousAnswer,
        previousQuestion
      }
    );
  }

  setValue(event) {
    let v = event.target.value.toLowerCase().trim().replace(/[^abcdefgs#♭']/g, '');
    if(v.charAt(1) === 'f') {
      v = v.charAt(0) + '♭';
    } else if(v.charAt(1) === 's') {
      v = v.charAt(0) + '#';
    }

    this.setState({value: v});
  }

  evaluateAnswer(answer) {
    let question = this.state.questionsAvailable[this.state.questionId];
    for(let i = 0; i < question.answer.length; i++) {
      if(question.answer[i] === answer) {
        this.nextQuestion(true);
        return true;
      }
    }
    this.nextQuestion(false);
    return false;
  }

  renderResponse() {
    if(this.state.isLastAnswerCorrect && this.state.answer != null) {
      return (

          Correct!

      );
    } else if(this.state.answer != null) {
      return (

          Wrong!

            Question: {this.state.previousQuestion}
 Answer: {this.state.previousAnswer}

      );
    }
  }

  showQuestion() {
    if (this.state.questionsAvailable.length > 0) {
      let question = this.state.questionsAvailable[this.state.questionId].question;
      return (

          {question}

      )
    } else {
      return (

You did it!

      );
    }
  }

  render() {
    return (

        <form onSubmit={this.submitAnswer.bind(this)}>
          {this.renderResponse()}
          {this.showQuestion()}
          <input type="text" maxLength="2" value={this.state.value} onChange={this.setValue.bind(this)}/>
        </form>

          Questions Left: {this.state.questionsAvailable.length}

    );
  }
}

export default CircleOfFifths;
<commit_msg>accuracy<commit_after>import React, { Component } from 'react';
import {AllQuestions} from './questions.js';

class CircleOfFifths extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLastAnswerCorrect: false,
      questionsAvailable: JSON.parse(JSON.stringify(AllQuestions)),
      answer: null,
      value: '',
      questionId: this.selectAQuestionId(AllQuestions.length),
      previousAnswer: '',
      previousQuestion: '',
      correctCount: 0,
      submissions: 0
    };

  }

  selectAQuestionId(amountLeft) {
    amountLeft = Math.max(amountLeft - 1, 0);
    return Math.floor((Math.random() * amountLeft));
  }

  submitAnswer(event) {
    event.preventDefault();
    if(this.state.value !== '' && this.state.questionsAvailable.length > 0) {
      let isLastAnswerCorrect = this.evaluateAnswer(this.state.value);
      let correctCount = this.state.correctCount;
      let submissions = this.state.submissions + 1;
      if(isLastAnswerCorrect) {
        correctCount += 1;
      }
      this.setState({
        answer: this.state.value,
        value: '',
        isLastAnswerCorrect,
        correctCount,
        submissions
      });
    }
  }

  nextQuestion(removeQuestion) {
    let questions = JSON.parse(JSON.stringify(this.state.questionsAvailable));
    if(removeQuestion) {
      questions.splice(this.state.questionId, 1);
    }
    let previousAnswer = this.state.questionsAvailable[this.state.questionId].answer[0];
    let previousQuestion = this.state.questionsAvailable[this.state.questionId].question;
    this.setState(
      {
        questionsAvailable: questions,
        questionId: this.selectAQuestionId(questions.length),
        previousAnswer,
        previousQuestion
      }
    );
  }

  setValue(event) {
    let v = event.target.value.toLowerCase().trim().replace(/[^abcdefgs#♭']/g, '');
    if(v.charAt(1) === 'f') {
      v = v.charAt(0) + '♭';
    } else if(v.charAt(1) === 's') {
      v = v.charAt(0) + '#';
    }

    this.setState({value: v});
  }

  evaluateAnswer(answer) {
    let question = this.state.questionsAvailable[this.state.questionId];
    for(let i = 0; i < question.answer.length; i++) {
      if(question.answer[i] === answer) {
        this.nextQuestion(true);
        return true;
      }
    }
    this.nextQuestion(false);
    return false;
  }

  renderResponse() {
    if(this.state.isLastAnswerCorrect && this.state.answer != null) {
      return (

          Correct!

      );
    } else if(this.state.answer != null) {
      return (

          Wrong!

            Question: {this.state.previousQuestion}
 Answer: {this.state.previousAnswer}

      );
    }
  }

  showQuestion() {
    if (this.state.questionsAvailable.length > 0) {
      let question = this.state.questionsAvailable[this.state.questionId].question;
      return (

          {question}

      )
    } else {
      return (

You did it!

      );
    }
  }

  render() {
    let accuracy = '';
    if (this.state.correctCount > 0) {
      accuracy = 'Accuracy: ' + parseInt(((parseFloat(this.state.correctCount) / parseFloat(this.state.submissions)) * 100), 10) + '%';
    }
    return (

        <form onSubmit={this.submitAnswer.bind(this)}>
          {this.renderResponse()}
          {this.showQuestion()}
          <input type="text" maxLength="2" value={this.state.value} onChange={this.setValue.bind(this)}/>
        </form>

          Questions Left: {this.state.questionsAvailable.length}

          {accuracy}

    );
  }
}

export default CircleOfFifths;
<|endoftext|>


<commit_before>import { utils } from './utils';
import path from 'path';
import request from 'request';
import requestDebug from 'request-debug';
import _ from 'lodash';
import basicMethods from './basicMethods';

export default class ClassyResource {
  constructor(Classy, urlData) {
    /** Public properties */
    this.basePath = Classy.basePath;
    this.baseUrl = Classy.baseUrl;
    this.path = utils.makeURLInterpolator(this.path || '');

    /** Private properties */
    this._classy = Classy;
    this._urlData = urlData;

    /** Request debug setting */
    if (this._classy.requestDebug) {
      requestDebug(request);
    }

    /** Add basic methods */
    if (urlData.basic) {
      this._addBasicMethods(urlData.basic);
    }

    /** Add list methods */
    if (urlData.lists) {
      this._addListMethods(urlData.lists);
    }

    /** Add create methods */
    if (urlData.creates) {
      this._addCreateMethods(urlData.creates);
    }

    /** Add custom methods */
    if (urlData.custom && urlData.custom.methods) {
      this._addCustomMethods(urlData.custom.methods);
    }
  }

  createMethod(spec) {
    const PARAM_REGEX = /\{(.*?)\}/g;

    /** Get parameterized request URL and method */
    const specPath = (!_.isUndefined(spec.path) ? spec.path : '');
    const fullPath = this._urlData.path + specPath;
    const commandPath = utils.makeURLInterpolator(fullPath);
    const urlParams = utils.getRegexMatches(fullPath, PARAM_REGEX);
    const requestMethod = _.get(spec, 'method', 'GET').toUpperCase();

    /** Return resource method */
    return (...args) => {

      /** Extract data from function arguments */
      const _this = this;
      const urlData = this._populateUrlParams(urlParams, args);
      const data = utils.getDataFromArgs(args);

      /** Error if not all required params are satisfied */
      for (let i = 0; i < urlParams.length; i++) {
        const arg = args[0];
        const param = urlParams[i];

        if (!arg) {
          throw new Error(
            'Classy: Argument "' + urlParams[i] + '" required, but got: ' + arg +
            ' (on API request to ' + requestMethod + ' ' + commandPath(urlData) + ')'
          );
        }
      }

      /** Create full request path with resolved params */
      const resolvedPath = commandPath(urlData);
      const isAuthRequest = utils.isAuthRequest(resolvedPath);
      const requestPath = this._createFullPath(resolvedPath, isAuthRequest);

      /** Populate form data for authorization requests */
      let form = false;
      if (isAuthRequest) {
        form = this._generateAuthForm(args);
      }

      /**
       * Token stuff & requests need to be synchronous with refresh
       */
      return this._chooseToken(form, data).then((response) => {
        const DEFAULT_REQUEST_HEADERS = {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        };
        let requestHeaders = this._classy.headers || _.merge(DEFAULT_REQUEST_HEADERS, spec.headers);

        if (!response) {
          return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
        }

        if (response === 'app') {
          return this._refreshAppToken().then((response) => {
            const appToken = _.get(this._classy.appToken, 'access_token', null);
            delete data.token;

            requestHeaders.Authorization = 'Bearer ' + appToken;

            return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
          }, (error) => {
            console.error('App token refresh failed: ', error);
          });
        }

        if (response === 'member') {
          const memberToken = _.get(data, 'token.access_token', false);
          delete data.token;

          requestHeaders.Authorization = 'Bearer ' + memberToken;

          return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
        }

      }, (error) => {
        throw new Error('No token defined. Expected memberToken object or `token: \'app\'`');
      });

    };
  }

  /**
   *
   */
  _chooseToken(form, data) {
    const dataToken = _.get(data, 'token', false);
    const clientCredentialsRequest = _.get(form, 'grant_type', false) === 'client_credentials';

    const promise = new Promise((resolve, reject) => {
      /** Unless it's a `client_credentials` request, pick a token */
      if (clientCredentialsRequest) {
        resolve(false);
      } else {
        if (!dataToken) {
          reject('No token defined. Expected memberToken object or `token: \'app\'`');
        } else if (dataToken === 'app') {
          resolve('app');
        } else {
          resolve('member');
        }

      }
    });

    return promise;
  }

  /**
   * If this._classy.appToken exists, just resolve,
   * if not, refresh and then resolve.
   * @return {[type]} [description]
   */
  _refreshAppToken() {
    const promise = new Promise((resolve, reject) => {
      if (!this._classy.appToken) {
        this._classy.getAppToken().then((response) => {
          this._classy.setAppToken(response);
          resolve(response);
        }, (error) => {
          reject(error);
        });
      } else {
        resolve(this._classy.appToken);
      }
    });

    return promise;
  }

  /**
   * Adds basic methods to the resource based on
   * the passed array.
   *
   * @param {array} basics A list of basic methods to add
   */
  _addBasicMethods(basics) {
    const _this = this;
    _.each(basics, (method) => {
      _this[method] = _this.createMethod(basicMethods[method]);
    });
  }

  /**
   * Adds list methods to the resource based on
   * the passed array. Lists are resources associated
   * with the called resource, e.g. campaigns.listFaqs(#)
   *
   * @param {array} lists A list of list methods to add
   */
  _addListMethods(lists) {
    const _this = this;

    _.each(lists, (method) => {
      const methodName = _.upperFirst(_.camelCase(method));

      _this['list' + methodName] = _this.createMethod({
        method: 'GET',
        path: '/{id}/' + method
      });
    });
  }

  /**
   * Adds list methods to the resource based on
   * the passed array. Lists are resources associated
   * with the called resource, e.g. campaigns.listFaqs(#)
   *
   * @param {array} lists A list of list methods to add
   */
  _addCreateMethods(lists) {
    const _this = this;

    _.each(lists, (method) => {
      const uppercaseMethod = _.upperFirst(_.camelCase(method));
      const methodName = uppercaseMethod.substr(0, uppercaseMethod.length - 1);

      _this['create' + methodName] = _this.createMethod({
        method: 'POST',
        path: '/{id}/' + method
      });
    });
  }

  _addCustomMethods(methods) {
    const _this = this;

    _.each(methods, (params, methodName) => {
      _this[methodName] = _this.createMethod(params);
    });
  }

  /**
   * Create a full path based on the populated
   * URI and the basePath defined when the Classy
   * instance was initialized. Returns something like
   * /2.0/campaigns/23456 or /oauth2/auth.
   *
   * @param  {string}  resolvedPath  The populated URI
   * @param  {Boolean} isAuthRequest Determines whether to append basePath
   * @return {string}                The full URI for the upcoming request
   */
  _createFullPath(resolvedPath, isAuthRequest) {
    const fullPath = path.join(
        isAuthRequest ? '' : this.basePath,
        resolvedPath
      ).replace(/\\/g, '/');

    const normalizedPath = path.normalize(fullPath);

    return normalizedPath;
  }

  /**
   * Maps URL params from the spec to arguments
   * used in the actual resource call.
   *
   * @param  {array} params URL params from the spec
   * @param  {array} args   Arguments used in the resource call
   * @return {object}        A key/value mapping between params and args
   */
  _populateUrlParams(params, args) {
    const urlData = {};

    for (let i in params) {
      if (!_.isUndefined(args[i])) {
        urlData[params[i]] = args[i];
      }
    }

    return urlData;
  }

  /**
   * Handles authentication requests by adding
   * the appropriate x-www-form-urlencoded data
   * to the request. Camel cased keys will be
   * converted to snake case.
   *
   * @param  {array} args
   * @return {object}      A form object for the request
   */
  _generateAuthForm(args) {
    const form = _.mapKeys(
      args[0],
      (value, key) => _.snakeCase(key).replace(/(\d)_/g, '$1')
    );
    const grantType = utils.generateOauthGrantType(args[0]);

    form.grant_type = grantType;
    form.client_id = form.client_id || this._classy.clientId;
    form.client_secret = form.client_secret || this._classy.clientSecret;

    return form;
  }

  /**
   * Makes a request to Classy's API.
   *
   * @param  {string} path    Request URI
   * @param  {string} method  Request method
   * @param  {object} headers Request headers
   * @param  {object} form    Request form (optional)
   * @return {promise}         Promise based on API request
   */
  _makeRequest(path, method, headers, form, data) {
    const _this = this;
    const promise = new Promise((resolve, reject) => {
      const requestParams = {
        baseUrl: this.baseUrl,
        uri: path,
        method: method,
        headers: headers,
        rejectUnauthorized: false,
        form: form
      };

      if (method === 'GET') {
        requestParams.qs = data;
      } else {
        requestParams.body = JSON.stringify(data);
      }

      request(requestParams, (err, response, body) => {
        if (err || !/^2/.test('' + response.statusCode)) {
          if (err instanceof Error) {
            reject({
              error: err.message
            });
          } else {
            reject(err ? JSON.parse(err) : response);
          }
        } else {
          body = JSON.parse(body);
          resolve(body);
        }
      });

    });

    return promise;
  }
}
<commit_msg>Update response hanlding to return Error objects<commit_after>import { utils } from './utils';
import path from 'path';
import request from 'request';
import requestDebug from 'request-debug';
import _ from 'lodash';
import basicMethods from './basicMethods';

export default class ClassyResource {
  constructor(Classy, urlData) {
    /** Public properties */
    this.basePath = Classy.basePath;
    this.baseUrl = Classy.baseUrl;
    this.path = utils.makeURLInterpolator(this.path || '');

    /** Private properties */
    this._classy = Classy;
    this._urlData = urlData;

    /** Request debug setting */
    if (this._classy.requestDebug) {
      requestDebug(request);
    }

    /** Add basic methods */
    if (urlData.basic) {
      this._addBasicMethods(urlData.basic);
    }

    /** Add list methods */
    if (urlData.lists) {
      this._addListMethods(urlData.lists);
    }

    /** Add create methods */
    if (urlData.creates) {
      this._addCreateMethods(urlData.creates);
    }

    /** Add custom methods */
    if (urlData.custom && urlData.custom.methods) {
      this._addCustomMethods(urlData.custom.methods);
    }
  }

  createMethod(spec) {
    const PARAM_REGEX = /\{(.*?)\}/g;

    /** Get parameterized request URL and method */
    const specPath = (!_.isUndefined(spec.path) ? spec.path : '');
    const fullPath = this._urlData.path + specPath;
    const commandPath = utils.makeURLInterpolator(fullPath);
    const urlParams = utils.getRegexMatches(fullPath, PARAM_REGEX);
    const requestMethod = _.get(spec, 'method', 'GET').toUpperCase();

    /** Return resource method */
    return (...args) => {

      /** Extract data from function arguments */
      const _this = this;
      const urlData = this._populateUrlParams(urlParams, args);
      const data = utils.getDataFromArgs(args);

      /** Error if not all required params are satisfied */
      for (let i = 0; i < urlParams.length; i++) {
        const arg = args[0];
        const param = urlParams[i];

        if (!arg) {
          throw new Error(
            'Classy: Argument "' + urlParams[i] + '" required, but got: ' + arg +
            ' (on API request to ' + requestMethod + ' ' + commandPath(urlData) + ')'
          );
        }
      }

      /** Create full request path with resolved params */
      const resolvedPath = commandPath(urlData);
      const isAuthRequest = utils.isAuthRequest(resolvedPath);
      const requestPath = this._createFullPath(resolvedPath, isAuthRequest);

      /** Populate form data for authorization requests */
      let form = false;
      if (isAuthRequest) {
        form = this._generateAuthForm(args);
      }

      /**
       * Token stuff & requests need to be synchronous with refresh
       */
      return this._chooseToken(form, data).then((response) => {
        const DEFAULT_REQUEST_HEADERS = {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        };
        let requestHeaders = this._classy.headers || _.merge(DEFAULT_REQUEST_HEADERS, spec.headers);

        if (!response) {
          return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
        }

        if (response === 'app') {
          return this._refreshAppToken().then((response) => {
            const appToken = _.get(this._classy.appToken, 'access_token', null);
            delete data.token;

            requestHeaders.Authorization = 'Bearer ' + appToken;

            return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
          }, (error) => {
            console.error('App token refresh failed: ', error);
          });
        }

        if (response === 'member') {
          const memberToken = _.get(data, 'token.access_token', false);
          delete data.token;

          requestHeaders.Authorization = 'Bearer ' + memberToken;

          return this._makeRequest(requestPath, requestMethod, requestHeaders, form, data);
        }

      }, (error) => {
        throw new Error('No token defined. Expected memberToken object or `token: \'app\'`');
      });

    };
  }

  /**
   *
   */
  _chooseToken(form, data) {
    const dataToken = _.get(data, 'token', false);
    const clientCredentialsRequest = _.get(form, 'grant_type', false) === 'client_credentials';

    const promise = new Promise((resolve, reject) => {
      /** Unless it's a `client_credentials` request, pick a token */
      if (clientCredentialsRequest) {
        resolve(false);
      } else {
        if (!dataToken) {
          reject('No token defined. Expected memberToken object or `token: \'app\'`');
        } else if (dataToken === 'app') {
          resolve('app');
        } else {
          resolve('member');
        }

      }
    });

    return promise;
  }

  /**
   * If this._classy.appToken exists, just resolve,
   * if not, refresh and then resolve.
   * @return {[type]} [description]
   */
  _refreshAppToken() {
    const promise = new Promise((resolve, reject) => {
      if (!this._classy.appToken) {
        this._classy.getAppToken().then((response) => {
          this._classy.setAppToken(response);
          resolve(response);
        }, (error) => {
          reject(error);
        });
      } else {
        resolve(this._classy.appToken);
      }
    });

    return promise;
  }

  /**
   * Adds basic methods to the resource based on
   * the passed array.
   *
   * @param {array} basics A list of basic methods to add
   */
  _addBasicMethods(basics) {
    const _this = this;
    _.each(basics, (method) => {
      _this[method] = _this.createMethod(basicMethods[method]);
    });
  }

  /**
   * Adds list methods to the resource based on
   * the passed array. Lists are resources associated
   * with the called resource, e.g. campaigns.listFaqs(#)
   *
   * @param {array} lists A list of list methods to add
   */
  _addListMethods(lists) {
    const _this = this;

    _.each(lists, (method) => {
      const methodName = _.upperFirst(_.camelCase(method));

      _this['list' + methodName] = _this.createMethod({
        method: 'GET',
        path: '/{id}/' + method
      });
    });
  }

  /**
   * Adds list methods to the resource based on
   * the passed array. Lists are resources associated
   * with the called resource, e.g. campaigns.listFaqs(#)
   *
   * @param {array} lists A list of list methods to add
   */
  _addCreateMethods(lists) {
    const _this = this;

    _.each(lists, (method) => {
      const uppercaseMethod = _.upperFirst(_.camelCase(method));
      const methodName = uppercaseMethod.substr(0, uppercaseMethod.length - 1);

      _this['create' + methodName] = _this.createMethod({
        method: 'POST',
        path: '/{id}/' + method
      });
    });
  }

  _addCustomMethods(methods) {
    const _this = this;

    _.each(methods, (params, methodName) => {
      _this[methodName] = _this.createMethod(params);
    });
  }

  /**
   * Create a full path based on the populated
   * URI and the basePath defined when the Classy
   * instance was initialized. Returns something like
   * /2.0/campaigns/23456 or /oauth2/auth.
   *
   * @param  {string}  resolvedPath  The populated URI
   * @param  {Boolean} isAuthRequest Determines whether to append basePath
   * @return {string}                The full URI for the upcoming request
   */
  _createFullPath(resolvedPath, isAuthRequest) {
    const fullPath = path.join(
        isAuthRequest ? '' : this.basePath,
        resolvedPath
      ).replace(/\\/g, '/');

    const normalizedPath = path.normalize(fullPath);

    return normalizedPath;
  }

  /**
   * Maps URL params from the spec to arguments
   * used in the actual resource call.
   *
   * @param  {array} params URL params from the spec
   * @param  {array} args   Arguments used in the resource call
   * @return {object}        A key/value mapping between params and args
   */
  _populateUrlParams(params, args) {
    const urlData = {};

    for (let i in params) {
      if (!_.isUndefined(args[i])) {
        urlData[params[i]] = args[i];
      }
    }

    return urlData;
  }

  /**
   * Handles authentication requests by adding
   * the appropriate x-www-form-urlencoded data
   * to the request. Camel cased keys will be
   * converted to snake case.
   *
   * @param  {array} args
   * @return {object}      A form object for the request
   */
  _generateAuthForm(args) {
    const form = _.mapKeys(
      args[0],
      (value, key) => _.snakeCase(key).replace(/(\d)_/g, '$1')
    );
    const grantType = utils.generateOauthGrantType(args[0]);

    form.grant_type = grantType;
    form.client_id = form.client_id || this._classy.clientId;
    form.client_secret = form.client_secret || this._classy.clientSecret;

    return form;
  }

  /**
   * Makes a request to Classy's API.
   *
   * @param  {string} path    Request URI
   * @param  {string} method  Request method
   * @param  {object} headers Request headers
   * @param  {object} form    Request form (optional)
   * @return {promise}         Promise based on API request
   */
  _makeRequest(path, method, headers, form, data) {
    const _this = this;
    const promise = new Promise((resolve, reject) => {
      const requestParams = {
        baseUrl: this.baseUrl,
        uri: path,
        method: method,
        headers: headers,
        rejectUnauthorized: false,
        form: form
      };

      if (method === 'GET') {
        requestParams.qs = data;
      } else {
        requestParams.body = JSON.stringify(data);
      }

      request(requestParams, (err, response, body) => {
        if (err || !/^2/.test('' + response.statusCode)) {
          let error;
          if (err && err instanceof Error) {
            error = err;
          } else if (err && !(err instanceof Error)) {
            error = new Error(err);
          } else {
            error = new Error(JSON.stringify(response));
          }

          reject(error);
        } else {
          body = JSON.parse(body);
          resolve(body);
        }
      });

    });

    return promise;
  }
}
<|endoftext|>


<commit_before>/*
 * @package info.ajaxplorer.plugins
 *
 * Copyright 2007-2009 Charles du Jeu
 * This file is part of AjaXplorer.
 * The latest code can be found at http://www.ajaxplorer.info/
 *
 * This program is published under the LGPL Gnu Lesser General Public License.
 * You should have received a copy of the license along with AjaXplorer.
 *
 * The main conditions are as follow :
 * You must conspicuously and appropriately publish on each copy distributed
 * an appropriate copyright notice and disclaimer of warranty and keep intact
 * all the notices that refer to this License and to the absence of any warranty;
 * and give any other recipients of the Program a copy of the GNU Lesser General
 * Public License along with the Program.
 *
 * If you modify your copy or copies of the library or any portion of it, you may
 * distribute the resulting library provided you do so under the GNU Lesser
 * General Public License. However, programs that link to the library may be
 * licensed under terms of your choice, so long as the library itself can be changed.
 * Any translation of the GNU Lesser General Public License must be accompanied by the
 * GNU Lesser General Public License.
 *
 * If you copy or distribute the program, you must accompany it with the complete
 * corresponding machine-readable source code or with a written offer, valid for at
 * least three years, to furnish the complete corresponding machine-readable source code.
 *
 * Any of the above conditions can be waived if you get permission from the copyright holder.
 * AjaXplorer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */
/**
 * The tree object. Encapsulate the webfx tree.
 */
Class.create("FoldersTree", AjxpPane, {

	__implements : ["IFocusable", "IContextMenuable"],

	/**
	 * Constructor
	 * @param $super klass Superclass reference
	 * @param oElement HTMLElement
	 * @param options Object
	 */
	initialize: function ($super, oElement, options)
	{
		$super(oElement, options);
		this.treeContainer = new Element('div', {id:'tree_container', style:'overflow:auto;height:100%;width:100%;'});
        if(this.options.replaceScroller){
            this.scroller = new Element('div', {id:'tree_scroller', className:'scroller_track'});
            this.scroller.insert('

');
            oElement.insert(this.scroller);
            this.treeContainer.setStyle({overflow:"hidden"});
        }
		oElement.insert(this.treeContainer);
		disableTextSelection(this.treeContainer);
        if(this.options.replaceScroller){
            this.scrollbar = new Control.ScrollBar('tree_container','tree_scroller');
            document.observe("ajaxplorer:tree_change", this.scrollbar.recalculateLayout.bind(this.scrollbar));
        }

		this.options = {};
		if(options){
			this.options = options;
		}
		var thisObject = this;
		var action = function(e){
			if(!ajaxplorer) return;
			ajaxplorer.focusOn(thisObject);
			if(this.ajxpNode){
				ajaxplorer.actionBar.fireDefaultAction("dir", this.ajxpNode);
			}
		};

		var filter = this.createFilter();
		var fakeRootNode = new AjxpNode("/", true, MessageHash[391], "folder.png");
		fakeRootNode._isLoaded = true;
		this.tree = new AJXPTree(fakeRootNode,  action, filter);

		this.treeContainer.update(this.tree.toString());
		$(this.tree.id).ajxpNode = this.tree.ajxpNode;
		$(this.tree.id).observe("click", function(e){
			this.action(e);
			Event.stop(e);
		}.bind(this.tree));
		AjxpDroppables.add(this.tree.id);
		if(!this.tree.open && !this.tree.loading) {
			this.tree.toggle();
		}
		this.treeContainer.observe("click", function(){
			ajaxplorer.focusOn(this);
		}.bind(this));

		this.rootNodeId = this.tree.id;
		this.hasFocus;

		document.observe("ajaxplorer:context_changed", function(event){
			var path = event.memo.getPath();
			window.setTimeout(function(e){
				this.setSelectedPath(path);
			}.bind(this), 100);
		}.bind(this) );

		document.observe("ajaxplorer:root_node_changed", function(event){
			var ajxpRootNode = event.memo;
			this.tree.setAjxpRootNode(ajxpRootNode);
			this.changeRootLabel(ajxpRootNode.getLabel(), ajxpRootNode.getIcon());
		}.bind(this));

		document.observe("ajaxplorer:component_config_changed", function(event){
			if(event.memo.className == "FoldersTree"){
				var config = event.memo.classConfig.get('all');
				var options = XPathSelectNodes(config, 'property');
				for(var i=0;i<options.length;i++){
					this.options[options[i].getAttribute('name')] = options[i].getAttribute('value');
				}
				if(this.tree){
					this.tree.filter = this.createFilter();
				}
			}
		}.bind(this) );

	},

	/**
	 * Create a filtering function based on the options display
	 * @returns Function
	 */
	createFilter : function(){
		var displayOptions = this.options.display || "dz";
		if(displayOptions.indexOf("a") > -1) displayOptions = "dzf";
		if(displayOptions.indexOf("z") > -1 && window.zipEnabled === false) displayOptions = displayOptions.split("z").join("");
		this.options.display  = displayOptions;

		var d = (displayOptions.indexOf("d") > -1);
		var z = (displayOptions.indexOf("z") > -1);
		var f = (displayOptions.indexOf("f") > -1);
		var filter = function(ajxpNode){
			return (((d && !ajxpNode.isLeaf()) || (f && ajxpNode.isLeaf()) || (z && (ajxpNode.getAjxpMime()=="zip" || ajxpNode.getAjxpMime()=="ajxp_browsable_archive"))) && (ajxpNode.getParent().getAjxpMime() != "ajxp_recycle"));
		};
		return filter;
	},

	/**
	 * Focus implementation of IAjxpWidget
	 */
	focus: function(){
		if(webFXTreeHandler.selected)
		{
			webFXTreeHandler.selected.focus();
		}
		webFXTreeHandler.setFocus(true);
		this.hasFocus = true;
	},

	/**
	 * Blur implementation of IAjxpWidget
	 */
	blur: function(){
		if(webFXTreeHandler.selected)
		{
			webFXTreeHandler.selected.blur();
		}
		webFXTreeHandler.setFocus(false);
		this.hasFocus = false;
	},

	/**
	 * Resize implementation of IAjxpWidget
	 */
	resize : function(){
		fitHeightToBottom(this.treeContainer, null);
        if(this.scrollbar){
            this.scroller.setStyle({height:parseInt(this.treeContainer.getHeight())+'px'});
            this.scrollbar.recalculateLayout();
        }
	},

	/**
	 * ShowElement implementation of IAjxpWidget
	 */
	showElement : function(show){
		if (show) this.treeContainer.show();
		else this.treeContainer.hide();
	},

	/**
	 * Sets the contextual menu
	 * @param protoMenu Proto.Menu
	 */
	setContextualMenu: function(protoMenu){
		Event.observe(this.rootNodeId+'-anchor', 'contextmenu', function(e){eval(this.action);}.bind(webFXTreeHandler.all[this.rootNodeId]));
		protoMenu.addElements('#'+this.rootNodeId+'-anchor');
		webFXTreeHandler.contextMenu = protoMenu;
	},

	/**
	 * Find a tree node by its path
	 * @param path String
	 * @returns WebFXTreeItem
	 */
	getNodeByPath : function(path){
		for(var key in webFXTreeHandler.all){
			if(webFXTreeHandler.all[key] && webFXTreeHandler.all[key].ajxpNode && webFXTreeHandler.all[key].ajxpNode.getPath() == path){
				return webFXTreeHandler.all[key];
			}
		}
	},

	/**
	 * Finds the node and select it
	 * @param path String
	 */
	setSelectedPath : function(path){
		if(path == "" || path == "/"){
			this.tree.select();
			return;
		}
		var parts = this.cleanPathToArray(path);
		var crtPath = "";
		for(var i=0;i<parts.length;i++){
			crtPath += "/" + parts[i];
			var node = this.getNodeByPath(crtPath);
			if(node && node.childNodes.length){
				node._webfxtree_expand();
			}
		}
		if(node){
			node.select();
		}
	},

	/**
	 * Transforms url to a path array
	 * @param url String
	 * @returns Array
	 */
	cleanPathToArray: function(url){
		var splitPath = url.split("/");
		var path = new Array();
		var j = 0;
		for(i=0; i<splitPath.length; i++)
		{
			if(splitPath[i] != '')
			{
				path[j] = splitPath[i];
				j++;
			}
		}
		return path;
	},

	/**
	 * Change the root node label
	 * @param newLabel String
	 * @param newIcon String
	 */
	changeRootLabel: function(newLabel, newIcon){
		this.changeNodeLabel(this.tree.id, newLabel, newIcon);
	},

	/**
	 * Change a node label
	 * @param nodeId String the Id of the node (webFX speaking)
	 * @param newLabel String
	 * @param newIcon String
	 */
	changeNodeLabel: function(nodeId, newLabel, newIcon){
		var node = $(nodeId+'-label').update(newLabel);
		if(newIcon){
			var realNode = webFXTreeHandler.all[nodeId];
			realNode.icon = newIcon;
			realNode.openIcon = newIcon;
		}
	}
});<commit_msg>fix top folder right-click pb<commit_after>/*
 * @package info.ajaxplorer.plugins
 *
 * Copyright 2007-2009 Charles du Jeu
 * This file is part of AjaXplorer.
 * The latest code can be found at http://www.ajaxplorer.info/
 *
 * This program is published under the LGPL Gnu Lesser General Public License.
 * You should have received a copy of the license along with AjaXplorer.
 *
 * The main conditions are as follow :
 * You must conspicuously and appropriately publish on each copy distributed
 * an appropriate copyright notice and disclaimer of warranty and keep intact
 * all the notices that refer to this License and to the absence of any warranty;
 * and give any other recipients of the Program a copy of the GNU Lesser General
 * Public License along with the Program.
 *
 * If you modify your copy or copies of the library or any portion of it, you may
 * distribute the resulting library provided you do so under the GNU Lesser
 * General Public License. However, programs that link to the library may be
 * licensed under terms of your choice, so long as the library itself can be changed.
 * Any translation of the GNU Lesser General Public License must be accompanied by the
 * GNU Lesser General Public License.
 *
 * If you copy or distribute the program, you must accompany it with the complete
 * corresponding machine-readable source code or with a written offer, valid for at
 * least three years, to furnish the complete corresponding machine-readable source code.
 *
 * Any of the above conditions can be waived if you get permission from the copyright holder.
 * AjaXplorer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */
/**
 * The tree object. Encapsulate the webfx tree.
 */
Class.create("FoldersTree", AjxpPane, {

	__implements : ["IFocusable", "IContextMenuable"],

	/**
	 * Constructor
	 * @param $super klass Superclass reference
	 * @param oElement HTMLElement
	 * @param options Object
	 */
	initialize: function ($super, oElement, options)
	{
		$super(oElement, options);
		this.treeContainer = new Element('div', {id:'tree_container', style:'overflow:auto;height:100%;width:100%;'});
        if(this.options.replaceScroller){
            this.scroller = new Element('div', {id:'tree_scroller', className:'scroller_track'});
            this.scroller.insert('

');
            oElement.insert(this.scroller);
            this.treeContainer.setStyle({overflow:"hidden"});
        }
		oElement.insert(this.treeContainer);
		disableTextSelection(this.treeContainer);
        if(this.options.replaceScroller){
            this.scrollbar = new Control.ScrollBar('tree_container','tree_scroller');
            document.observe("ajaxplorer:tree_change", this.scrollbar.recalculateLayout.bind(this.scrollbar));
        }

		this.options = {};
		if(options){
			this.options = options;
		}
		var thisObject = this;
		var action = function(e){
			if(!ajaxplorer) return;
			ajaxplorer.focusOn(thisObject);
			if(this.ajxpNode){
				ajaxplorer.actionBar.fireDefaultAction("dir", this.ajxpNode);
			}
		};

		var filter = this.createFilter();
		var fakeRootNode = new AjxpNode("/", true, MessageHash[391], "folder.png");
		fakeRootNode._isLoaded = true;
		this.tree = new AJXPTree(fakeRootNode,  action, filter);

		this.treeContainer.update(this.tree.toString());
		$(this.tree.id).ajxpNode = this.tree.ajxpNode;
		$(this.tree.id).observe("click", function(e){
			this.action(e);
			Event.stop(e);
		}.bind(this.tree));

		AjxpDroppables.add(this.tree.id);
		if(!this.tree.open && !this.tree.loading) {
			this.tree.toggle();
		}
		this.treeContainer.observe("click", function(){
			ajaxplorer.focusOn(this);
		}.bind(this));

		this.rootNodeId = this.tree.id;
		this.hasFocus;

		document.observe("ajaxplorer:context_changed", function(event){
			var path = event.memo.getPath();
			window.setTimeout(function(e){
				this.setSelectedPath(path);
			}.bind(this), 100);
		}.bind(this) );

		document.observe("ajaxplorer:root_node_changed", function(event){
			var ajxpRootNode = event.memo;
			this.tree.setAjxpRootNode(ajxpRootNode);
			this.changeRootLabel(ajxpRootNode.getLabel(), ajxpRootNode.getIcon());
		}.bind(this));

		document.observe("ajaxplorer:component_config_changed", function(event){
			if(event.memo.className == "FoldersTree"){
				var config = event.memo.classConfig.get('all');
				var options = XPathSelectNodes(config, 'property');
				for(var i=0;i<options.length;i++){
					this.options[options[i].getAttribute('name')] = options[i].getAttribute('value');
				}
				if(this.tree){
					this.tree.filter = this.createFilter();
				}
			}
		}.bind(this) );

	},

	/**
	 * Create a filtering function based on the options display
	 * @returns Function
	 */
	createFilter : function(){
		var displayOptions = this.options.display || "dz";
		if(displayOptions.indexOf("a") > -1) displayOptions = "dzf";
		if(displayOptions.indexOf("z") > -1 && window.zipEnabled === false) displayOptions = displayOptions.split("z").join("");
		this.options.display  = displayOptions;

		var d = (displayOptions.indexOf("d") > -1);
		var z = (displayOptions.indexOf("z") > -1);
		var f = (displayOptions.indexOf("f") > -1);
		var filter = function(ajxpNode){
			return (((d && !ajxpNode.isLeaf()) || (f && ajxpNode.isLeaf()) || (z && (ajxpNode.getAjxpMime()=="zip" || ajxpNode.getAjxpMime()=="ajxp_browsable_archive"))) && (ajxpNode.getParent().getAjxpMime() != "ajxp_recycle"));
		};
		return filter;
	},

	/**
	 * Focus implementation of IAjxpWidget
	 */
	focus: function(){
		if(webFXTreeHandler.selected)
		{
			webFXTreeHandler.selected.focus();
		}
		webFXTreeHandler.setFocus(true);
		this.hasFocus = true;
	},

	/**
	 * Blur implementation of IAjxpWidget
	 */
	blur: function(){
		if(webFXTreeHandler.selected)
		{
			webFXTreeHandler.selected.blur();
		}
		webFXTreeHandler.setFocus(false);
		this.hasFocus = false;
	},

	/**
	 * Resize implementation of IAjxpWidget
	 */
	resize : function(){
		fitHeightToBottom(this.treeContainer, null);
        if(this.scrollbar){
            this.scroller.setStyle({height:parseInt(this.treeContainer.getHeight())+'px'});
            this.scrollbar.recalculateLayout();
        }
	},

	/**
	 * ShowElement implementation of IAjxpWidget
	 */
	showElement : function(show){
		if (show) this.treeContainer.show();
		else this.treeContainer.hide();
	},

	/**
	 * Sets the contextual menu
	 * @param protoMenu Proto.Menu
	 */
	setContextualMenu: function(protoMenu){
        Event.observe(this.rootNodeId+'','contextmenu', function(event){
            this.select();
            this.action();
            Event.stop(event);
        }.bind(webFXTreeHandler.all[this.rootNodeId]));
         protoMenu.addElements('#'+this.rootNodeId+'');
		webFXTreeHandler.contextMenu = protoMenu;
	},

	/**
	 * Find a tree node by its path
	 * @param path String
	 * @returns WebFXTreeItem
	 */
	getNodeByPath : function(path){
		for(var key in webFXTreeHandler.all){
			if(webFXTreeHandler.all[key] && webFXTreeHandler.all[key].ajxpNode && webFXTreeHandler.all[key].ajxpNode.getPath() == path){
				return webFXTreeHandler.all[key];
			}
		}
	},

	/**
	 * Finds the node and select it
	 * @param path String
	 */
	setSelectedPath : function(path){
		if(path == "" || path == "/"){
			this.tree.select();
			return;
		}
		var parts = this.cleanPathToArray(path);
		var crtPath = "";
		for(var i=0;i<parts.length;i++){
			crtPath += "/" + parts[i];
			var node = this.getNodeByPath(crtPath);
			if(node && node.childNodes.length){
				node._webfxtree_expand();
			}
		}
		if(node){
			node.select();
		}
	},

	/**
	 * Transforms url to a path array
	 * @param url String
	 * @returns Array
	 */
	cleanPathToArray: function(url){
		var splitPath = url.split("/");
		var path = new Array();
		var j = 0;
		for(i=0; i<splitPath.length; i++)
		{
			if(splitPath[i] != '')
			{
				path[j] = splitPath[i];
				j++;
			}
		}
		return path;
	},

	/**
	 * Change the root node label
	 * @param newLabel String
	 * @param newIcon String
	 */
	changeRootLabel: function(newLabel, newIcon){
		this.changeNodeLabel(this.tree.id, newLabel, newIcon);
	},

	/**
	 * Change a node label
	 * @param nodeId String the Id of the node (webFX speaking)
	 * @param newLabel String
	 * @param newIcon String
	 */
	changeNodeLabel: function(nodeId, newLabel, newIcon){
		var node = $(nodeId+'-label').update(newLabel);
		if(newIcon){
			var realNode = webFXTreeHandler.all[nodeId];
			realNode.icon = newIcon;
			realNode.openIcon = newIcon;
		}
	}
});<|endoftext|>


<commit_before>import {Button} from 'widget/button'
import {ButtonGroup} from 'widget/buttonGroup'
import {Form} from 'widget/form/form'
import {Input} from 'widget/input'
import {ScrollableList} from 'widget/list'
import {Subject, fromEvent} from 'rxjs'
import {compose} from 'compose'
import {connect} from 'store'
import {delay} from 'rxjs/operators'
import {isMobile} from 'widget/userAgent'
import {selectFrom} from 'stateUtils'
import AutoFocus from 'widget/autoFocus'
import FloatingBox from 'widget/floatingBox'
import Keybinding from 'widget/keybinding'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import escapeStringRegexp from 'escape-string-regexp'
import styles from './combo.module.css'
import withSubscriptions from 'subscription'

const SELECTION_DELAY_MS = 350

const mapStateToProps = state => ({
    dimensions: selectFrom(state, 'dimensions') || []
})

class _Combo extends React.Component {
    inputContainer = React.createRef()
    input = React.createRef()
    list = React.createRef()
    select$ = new Subject()
    state = {
        showOptions: false,
        filter: '',
        filteredOptions: [],
        flattenedOptions: [],
        selectedOption: null,
        selected: false,
        focused: false
    }

    isActive() {
        const {busyMessage, disabled} = this.props
        return !(disabled || busyMessage)
    }

    render() {
        const {errorMessage, busyMessage, standalone, disabled, className, onCancel} = this.props
        const {label, tooltip, tooltipPlacement} = this.props
        const {showOptions} = this.state
        const onClick = e => {
            if (this.isActive()) {
                return standalone
                    ? onCancel && onCancel(e)
                    : showOptions
                        ? this.hideOptions()
                        : this.showOptions()
            }
        }
        return (
            <Form.FieldSet
                className={[styles.container, className].join(' ')}
                label={label}
                tooltip={tooltip}
                tooltipPlacement={tooltipPlacement}
                disabled={disabled}
                errorMessage={errorMessage}
                busyMessage={busyMessage}>
                <div
                    ref={this.inputContainer}
                    onClick={onClick}>
                    {this.renderInput()}
                </div>
                {showOptions ? this.renderOptions() : null}
            </Form.FieldSet>
        )
    }

    renderInput() {
        const {placeholder, autoFocus, standalone, readOnly, inputClassName, onBlur} = this.props
        const {focused, filter, selectedOption, showOptions} = this.state
        const showOptionsKeyBinding = showOptions ? undefined : () => this.showOptions()
        const keymap = {
            ArrowUp: showOptionsKeyBinding,
            ArrowDown: showOptionsKeyBinding,
            ArrowLeft: showOptionsKeyBinding,
            ArrowRight: showOptionsKeyBinding,
            Home: showOptionsKeyBinding,
            End: showOptionsKeyBinding
        }
        return (
            <Keybinding
                disabled={!this.isActive() || !focused}
                keymap={keymap}>
                <Input
                    ref={this.input}
                    className={[
                        styles.input,
                        standalone ? styles.standalone : null,
                        selectedOption && !showOptions ? styles.fakePlaceholder : null,
                        inputClassName
                    ].join(' ')}
                    type='search'
                    value={filter}
                    placeholder={selectedOption && !standalone ? selectedOption.label : placeholder}
                    disabled={!this.isActive()}
                    readOnly={readOnly || isMobile()}
                    rightComponent={this.renderButtons()}
                    onChange={e => this.setFilter(e.target.value)}
                    onFocus={() => this.setState({focused: true})}
                    onBlur={e => {
                        onBlur && onBlur(e)
                        this.setState({focused: false})
                    }}
                />
                <AutoFocus ref={this.input} enabled={autoFocus}/>
            </Keybinding>
        )
    }

    renderButtons() {
        return (
            <ButtonGroup layout='horizontal-nowrap'>
                {this.renderClearButton()}
                {this.renderToggleOptionsButton()}
            </ButtonGroup>
        )
    }

    renderClearButton() {
        const {allowClear} = this.props
        const {selectedOption} = this.state
        return allowClear && selectedOption
            ? (
                <Button
                    chromeless
                    shape='none'
                    air='none'
                    icon='times'
                    iconFixedWidth
                    onClick={() => this.select$.next()}
                />
            )
            : null
    }

    renderToggleOptionsButton() {
        const {placement} = this.props
        const {showOptions} = this.state
        const icon = {
            below: 'chevron-down',
            above: 'chevron-up'
        }
        return (
            <Button
                chromeless
                shape='none'
                air='none'
                icon={icon[placement]}
                iconFlipVertical={showOptions}
                iconFixedWidth
                disabled={!this.isActive()}
                onClick={() => showOptions
                    ? this.hideOptions()
                    : this.showOptions()
                }
            />
        )
    }

    renderOptions() {
        const {placement, optionsClassName, optionTooltipPlacement, alignment} = this.props
        const {flattenedOptions, selectedOption, selected} = this.state
        return (
            <FloatingBox
                element={this.inputContainer.current}
                placement={placement}>
                <ScrollableList
                    ref={this.list}
                    air='more'
                    className={optionsClassName || styles.options}
                    options={flattenedOptions}
                    selectedOption={selectedOption}
                    onSelect={option => this.select$.next(option)}
                    onCancel={() => this.resetFilter()}
                    autoCenter={!selected}
                    tooltipPlacement={optionTooltipPlacement}
                    autoHighlight
                    keyboard
                    alignment={alignment}
                />
            </FloatingBox>
        )
    }

    showOptions() {
        this.setState({showOptions: true})
    }

    hideOptions() {
        this.setState({showOptions: false})
    }

    setFilter(filter = '') {
        const {standalone} = this.props
        this.setState({
            showOptions: !!filter || standalone,
            filter
        }, this.updateOptions)
    }

    resetFilter() {
        const {onCancel, standalone} = this.props
        const {filter} = this.state
        if (standalone && onCancel && !filter) {
            onCancel()
        } else {
            this.setFilter()
        }
    }

    setSelectedOption(selectedOption) {
        this.updateState({
            selectedOption,
            selected: !!selectedOption
        })
    }

    updateState(state, callback) {
        const updatedState = (prevState, state) =>
            _.isEqual(_.pick(prevState, _.keys(state)), state) ? null : state
        this.setState(
            prevState =>
                updatedState(prevState, _.isFunction(state) ? state(prevState) : state),
            callback
        )
    }

    handleSelect() {
        const {onChange, addSubscription} = this.props
        addSubscription(
            this.select$.subscribe(
                option => {
                    this.setSelectedOption(option)
                    onChange && onChange(option)
                }
            ),
            this.select$.pipe(
                delay(SELECTION_DELAY_MS)
            ).subscribe(
                () => this.setState({selected: false}, this.setFilter)
            )
        )
    }

    handleBlur() {
        const {onCancel, addSubscription} = this.props
        const click$ = fromEvent(document, 'click')
        const isInputClick = e => this.inputContainer.current && this.inputContainer.current.contains(e.target)
        const isListClick = e => this.list.current && this.list.current.contains && this.list.current.contains(e.target)
        addSubscription(
            click$.subscribe(e => {
                if (!isInputClick(e) && !isListClick(e)) {
                    this.setFilter()
                    onCancel && onCancel(e)
                }
            })
        )
    }

    componentDidMount() {
        this.setFilter()
        this.handleSelect()
        this.handleBlur()
    }

    shouldComponentUpdate() {
        return !this.state.selected
    }

    componentDidUpdate() {
        this.updateOptions()
    }

    matcher(filter) {
        // match beginning of multiple words in order (e.g. "u k" matches "United Kingdom")
        return RegExp(filter.split(/\s/).map(part => `\\b${escapeStringRegexp(part)}`).join('.*'), 'i')
    }

    updateOptions() {
        const {options} = this.props
        const {filter} = this.state
        const matcher = this.matcher(filter)
        const getFilteredOptions = options =>
            _.compact(
                options.map(option =>
                    option.options
                        ? {...option, options: getFilteredOptions(option.options)}
                        : matcher.test(option.searchableText || option.label)
                            ? option
                            : null
                )
            )
        const getFlattenedOptions = options =>
            _.flatten(
                options.map(option =>
                    option.options
                        ? [{label: option.label, group: true}, ...option.options]
                        : option
                )
            )
        const filteredOptions = getFilteredOptions(options)
        const flattenedOptions = getFlattenedOptions(filteredOptions)

        const getInputOption = () => {
            const {value} = this.props
            return flattenedOptions && flattenedOptions.find(option => option.value === value)
        }

        const getSelectedOption = selectedOption => {
            const validatedSelectedOption = selectedOption && flattenedOptions.find(option => option.value === selectedOption.value)
            return validatedSelectedOption || getInputOption()
        }

        this.updateState(prevState => ({
            filteredOptions,
            flattenedOptions,
            selectedOption: getSelectedOption.bind(this)(prevState.selectedOption)
        }))
    }
}

export const Combo = compose(
    _Combo,
    withSubscriptions(),
    connect(mapStateToProps)
)

Combo.propTypes = {
    options: PropTypes.any.isRequired,
    alignment: PropTypes.oneOf(['left', 'center', 'right']),
    allowClear: PropTypes.any,
    autoFocus: PropTypes.any,
    busyMessage: PropTypes.any,
    className: PropTypes.string,
    disabled: PropTypes.any,
    errorMessage: PropTypes.any,
    inputClassName: PropTypes.string,
    keyboard: PropTypes.any,
    label: PropTypes.string,
    optionsClassName: PropTypes.string,
    optionTooltipPlacement: PropTypes.string,
    placeholder: PropTypes.string,
    placement: PropTypes.oneOf(['above', 'below']),
    readOnly: PropTypes.any,
    standalone: PropTypes.any,
    tooltip: PropTypes.string,
    tooltipPlacement: PropTypes.string,
    value: PropTypes.any,
    onBlur: PropTypes.func,
    onCancel: PropTypes.func,
    onChange: PropTypes.func
}

Combo.defaultProps = {
    alignment: 'left',
    placement: 'below',
    tooltipPlacement: 'top'
}
<commit_msg>Cleanup.<commit_after>import {Button} from 'widget/button'
import {ButtonGroup} from 'widget/buttonGroup'
import {Form} from 'widget/form/form'
import {Input} from 'widget/input'
import {ScrollableList} from 'widget/list'
import {Subject, fromEvent} from 'rxjs'
import {compose} from 'compose'
import {connect} from 'store'
import {delay} from 'rxjs/operators'
import {isMobile} from 'widget/userAgent'
import {selectFrom} from 'stateUtils'
import AutoFocus from 'widget/autoFocus'
import FloatingBox from 'widget/floatingBox'
import Keybinding from 'widget/keybinding'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import escapeStringRegexp from 'escape-string-regexp'
import styles from './combo.module.css'
import withSubscriptions from 'subscription'

const SELECTION_DELAY_MS = 350

const mapStateToProps = state => ({
    dimensions: selectFrom(state, 'dimensions') || []
})

class _Combo extends React.Component {
    inputContainer = React.createRef()
    input = React.createRef()
    list = React.createRef()
    select$ = new Subject()
    state = {
        showOptions: false,
        filter: '',
        filteredOptions: [],
        flattenedOptions: [],
        selectedOption: null,
        selected: false,
        focused: false
    }

    isActive() {
        const {busyMessage, disabled} = this.props
        return !(disabled || busyMessage)
    }

    render() {
        const {errorMessage, busyMessage, standalone, disabled, className, label, tooltip, tooltipPlacement, onCancel} = this.props
        const {showOptions} = this.state
        const onClick = e => {
            if (this.isActive()) {
                return standalone
                    ? onCancel && onCancel(e)
                    : showOptions
                        ? this.hideOptions()
                        : this.showOptions()
            }
        }
        return (
            <Form.FieldSet
                className={[styles.container, className].join(' ')}
                label={label}
                tooltip={tooltip}
                tooltipPlacement={tooltipPlacement}
                disabled={disabled}
                errorMessage={errorMessage}
                busyMessage={busyMessage}>
                <div
                    ref={this.inputContainer}
                    onClick={onClick}>
                    {this.renderInput()}
                </div>
                {showOptions ? this.renderOptions() : null}
            </Form.FieldSet>
        )
    }

    renderInput() {
        const {placeholder, autoFocus, standalone, readOnly, inputClassName, onBlur} = this.props
        const {focused, filter, selectedOption, showOptions} = this.state
        const showOptionsKeyBinding = showOptions ? undefined : () => this.showOptions()
        const keymap = {
            ArrowUp: showOptionsKeyBinding,
            ArrowDown: showOptionsKeyBinding,
            ArrowLeft: showOptionsKeyBinding,
            ArrowRight: showOptionsKeyBinding,
            Home: showOptionsKeyBinding,
            End: showOptionsKeyBinding
        }
        return (
            <Keybinding
                disabled={!this.isActive() || !focused}
                keymap={keymap}>
                <Input
                    ref={this.input}
                    className={[
                        styles.input,
                        standalone ? styles.standalone : null,
                        selectedOption && !showOptions ? styles.fakePlaceholder : null,
                        inputClassName
                    ].join(' ')}
                    type='search'
                    value={filter}
                    placeholder={selectedOption && !standalone ? selectedOption.label : placeholder}
                    disabled={!this.isActive()}
                    readOnly={readOnly || isMobile()}
                    rightComponent={this.renderButtons()}
                    onChange={e => this.setFilter(e.target.value)}
                    onFocus={() => this.setState({focused: true})}
                    onBlur={e => {
                        onBlur && onBlur(e)
                        this.setState({focused: false})
                    }}
                />
                <AutoFocus ref={this.input} enabled={autoFocus}/>
            </Keybinding>
        )
    }

    renderButtons() {
        return (
            <ButtonGroup layout='horizontal-nowrap'>
                {this.renderClearButton()}
                {this.renderToggleOptionsButton()}
            </ButtonGroup>
        )
    }

    renderClearButton() {
        const {allowClear} = this.props
        const {selectedOption} = this.state
        return allowClear && selectedOption
            ? (
                <Button
                    chromeless
                    shape='none'
                    air='none'
                    icon='times'
                    iconFixedWidth
                    onClick={() => this.select$.next()}
                />
            )
            : null
    }

    renderToggleOptionsButton() {
        const {placement} = this.props
        const {showOptions} = this.state
        const icon = {
            below: 'chevron-down',
            above: 'chevron-up'
        }
        return (
            <Button
                chromeless
                shape='none'
                air='none'
                icon={icon[placement]}
                iconFlipVertical={showOptions}
                iconFixedWidth
                disabled={!this.isActive()}
                onClick={() => showOptions
                    ? this.hideOptions()
                    : this.showOptions()
                }
            />
        )
    }

    renderOptions() {
        const {placement, optionsClassName, optionTooltipPlacement, alignment} = this.props
        const {flattenedOptions, selectedOption, selected} = this.state
        return (
            <FloatingBox
                element={this.inputContainer.current}
                placement={placement}>
                <ScrollableList
                    ref={this.list}
                    air='more'
                    className={optionsClassName || styles.options}
                    options={flattenedOptions}
                    selectedOption={selectedOption}
                    onSelect={option => this.select$.next(option)}
                    onCancel={() => this.resetFilter()}
                    autoCenter={!selected}
                    tooltipPlacement={optionTooltipPlacement}
                    autoHighlight
                    keyboard
                    alignment={alignment}
                />
            </FloatingBox>
        )
    }

    showOptions() {
        this.setState({showOptions: true})
    }

    hideOptions() {
        this.setState({showOptions: false})
    }

    setFilter(filter = '') {
        const {standalone} = this.props
        this.setState({
            showOptions: !!filter || standalone,
            filter
        }, this.updateOptions)
    }

    resetFilter() {
        const {onCancel, standalone} = this.props
        const {filter} = this.state
        if (standalone && onCancel && !filter) {
            onCancel()
        } else {
            this.setFilter()
        }
    }

    setSelectedOption(selectedOption) {
        this.updateState({
            selectedOption,
            selected: !!selectedOption
        })
    }

    updateState(state, callback) {
        const updatedState = (prevState, state) =>
            _.isEqual(_.pick(prevState, _.keys(state)), state) ? null : state
        this.setState(
            prevState =>
                updatedState(prevState, _.isFunction(state) ? state(prevState) : state),
            callback
        )
    }

    handleSelect() {
        const {onChange, addSubscription} = this.props
        addSubscription(
            this.select$.subscribe(
                option => {
                    this.setSelectedOption(option)
                    onChange && onChange(option)
                }
            ),
            this.select$.pipe(
                delay(SELECTION_DELAY_MS)
            ).subscribe(
                () => this.setState({selected: false}, this.setFilter)
            )
        )
    }

    handleBlur() {
        const {onCancel, addSubscription} = this.props
        const click$ = fromEvent(document, 'click')
        const isInputClick = e => this.inputContainer.current && this.inputContainer.current.contains(e.target)
        const isListClick = e => this.list.current && this.list.current.contains && this.list.current.contains(e.target)
        addSubscription(
            click$.subscribe(e => {
                if (!isInputClick(e) && !isListClick(e)) {
                    this.setFilter()
                    onCancel && onCancel(e)
                }
            })
        )
    }

    componentDidMount() {
        this.setFilter()
        this.handleSelect()
        this.handleBlur()
    }

    shouldComponentUpdate() {
        return !this.state.selected
    }

    componentDidUpdate() {
        this.updateOptions()
    }

    matcher(filter) {
        // match beginning of multiple words in order (e.g. "u k" matches "United Kingdom")
        return RegExp(filter.split(/\s/).map(part => `\\b${escapeStringRegexp(part)}`).join('.*'), 'i')
    }

    updateOptions() {
        const {options} = this.props
        const {filter} = this.state
        const matcher = this.matcher(filter)
        const getFilteredOptions = options =>
            _.compact(
                options.map(option =>
                    option.options
                        ? {...option, options: getFilteredOptions(option.options)}
                        : matcher.test(option.searchableText || option.label)
                            ? option
                            : null
                )
            )
        const getFlattenedOptions = options =>
            _.flatten(
                options.map(option =>
                    option.options
                        ? [{label: option.label, group: true}, ...option.options]
                        : option
                )
            )
        const filteredOptions = getFilteredOptions(options)
        const flattenedOptions = getFlattenedOptions(filteredOptions)

        const getInputOption = () => {
            const {value} = this.props
            return flattenedOptions && flattenedOptions.find(option => option.value === value)
        }

        const getSelectedOption = selectedOption => {
            const validatedSelectedOption = selectedOption && flattenedOptions.find(option => option.value === selectedOption.value)
            return validatedSelectedOption || getInputOption()
        }

        this.updateState(prevState => ({
            filteredOptions,
            flattenedOptions,
            selectedOption: getSelectedOption.bind(this)(prevState.selectedOption)
        }))
    }
}

export const Combo = compose(
    _Combo,
    withSubscriptions(),
    connect(mapStateToProps)
)

Combo.propTypes = {
    options: PropTypes.any.isRequired,
    alignment: PropTypes.oneOf(['left', 'center', 'right']),
    allowClear: PropTypes.any,
    autoFocus: PropTypes.any,
    busyMessage: PropTypes.any,
    className: PropTypes.string,
    disabled: PropTypes.any,
    errorMessage: PropTypes.any,
    inputClassName: PropTypes.string,
    keyboard: PropTypes.any,
    label: PropTypes.string,
    optionsClassName: PropTypes.string,
    optionTooltipPlacement: PropTypes.string,
    placeholder: PropTypes.string,
    placement: PropTypes.oneOf(['above', 'below']),
    readOnly: PropTypes.any,
    standalone: PropTypes.any,
    tooltip: PropTypes.string,
    tooltipPlacement: PropTypes.string,
    value: PropTypes.any,
    onBlur: PropTypes.func,
    onCancel: PropTypes.func,
    onChange: PropTypes.func
}

Combo.defaultProps = {
    alignment: 'left',
    placement: 'below',
    tooltipPlacement: 'top'
}
<|endoftext|>


<commit_before>/*
* Copyright 2011 eBay Software Foundation
*
* 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.
*/

'use strict';
var escapeXml = require('raptor-util/escapeXml');
var escapeXmlAttr = escapeXml.attr;
var runtime = require('./'); // Circular dependency, but that is okay
var extend = require('raptor-util/extend');
var attr = require('raptor-util/attr');
var attrs = require('raptor-util/attrs');
var forEach = require('raptor-util/forEach');
var markoRegExp = /.html|\.marko(.xml|.html)?$/;
var arrayFromArguments = require('raptor-util/arrayFromArguments');
var logger = require('raptor-logging').logger(module);

var viewEngine;
var req = require;

try {
    viewEngine = req('view-engine');
} catch(e) {}

function notEmpty(o) {
    if (o == null) {
        return false;
    } else if (Array.isArray(o)) {
        return !!o.length;
    } else if (o === '') {
        return false;
    }

    return true;
}

function createDeferredRenderer(handler) {
    function deferredRenderer(input, out) {
        deferredRenderer.renderer(input, out);
    }

    // This is the initial function that will do the rendering. We replace
    // the renderer with the actual renderer func on the first render
    deferredRenderer.renderer = function(input, out) {
        var rendererFunc = handler.renderer || handler.render;
        if (typeof renderFunc !== 'function') {
            throw new Error('Invalid tag handler: ' + handler);
        }
        // Use the actual renderer from now on
        deferredRenderer.renderer = rendererFunc;
        rendererFunc(input, out);
    };

    return deferredRenderer;
}

var WARNED_INVOKE_BODY = 0;

module.exports = {
    /**
     * Internal helper method to prevent null/undefined from being written out
     * when writing text that resolves to null/undefined
     * @private
     */
    s: function(str) {
        return (str == null) ? '' : str;
    },
    /**
     * Internal helper method to handle loops with a status variable
     * @private
     */
    fv: function (array, callback) {
        if (!array) {
            return;
        }
        if (!array.forEach) {
            array = [array];
        }
        var i = 0;
        var len = array.length;
        var loopStatus = {
                getLength: function () {
                    return len;
                },
                isLast: function () {
                    return i === len - 1;
                },
                isFirst: function () {
                    return i === 0;
                },
                getIndex: function () {
                    return i;
                }
            };
        for (; i < len; i++) {
            var o = array[i];
            callback(o || '', loopStatus);
        }
    },
    /**
     * Internal helper method to handle loops without a status variable
     * @private
     */
    f: forEach,
    /**
     * Internal helper method to handle native for loops
     * @private
     */
    fl: function (array, func) {
        if (array != null) {
            if (!Array.isArray(array)) {
                array = [array];
            }
            func(array, 0, array.length);
        }
    },
    /**
     * Internal helper method for looping over the properties of any object
     * @private
     */
    fp: function (o, func) {
        if (!o) {
            return;
        }
        for (var k in o) {
            if (o.hasOwnProperty(k)) {
                func(k, o[k]);
            }
        }
    },
    /**
     * Internal method to check if an object/array is empty
     * @private
     */
    e: function (o) {
        return !notEmpty(o);
    },
    /**
     * Internal method to check if an object/array is not empty
     * @private
     */
    ne: notEmpty,
    /**
     * Internal method to escape special XML characters
     * @private
     */
    x: escapeXml,
    /**
     * Internal method to escape special XML characters within an attribute
     * @private
     */
    xa: escapeXmlAttr,
    /**
     * Internal helper to prevent an object from being escaped
     * @private
     */
    nx: function (str) {
        return {
            toString: function () {
                return str;
            }
        };
    },
    /**
     * Internal method to render a single HTML attribute
     * @private
     */
    a: attr,

    /**
     * Internal method to render multiple HTML attributes based on the properties of an object
     * @private
     */
    as: attrs,
    /**
     * Loads a template
     */
    l: function(path, req) {
        if (typeof path === 'string') {
            if (path.charAt(0) === '.' && req.resolve) { // Check if the path is relative
                // The path is relative so use require.resolve to fully resolve the path
                path = req.resolve(path);
            }

            if (!viewEngine || markoRegExp.test(path)) {
                return runtime.load(path);
            } else {
                return viewEngine.load(path);
            }
        } else if (path.render) {
            // Assume it is already a pre-loaded template
            return path;
        } else {
            return runtime.load(path);
        }
    },
    /**
     * Returns the render function for a tag handler
     */
    r: function(handler) {
        var renderFunc = handler.renderer || handler.render || handler;

        // If the user code has a circular function then the renderer function
        // may not be available on the module. Since we can't get a reference
        // to the actual renderer(input, out) function right now we lazily
        // try to get access to it later.
        if (typeof renderFunc !== 'function') {
            return createDeferredRenderer(handler);
        }

        return renderFunc;
    },

    // ----------------------------------
    // The helpers listed below require an out
    // ----------------------------------

    /**
     * Invoke a tag handler render function
     */
    t: function (out, renderFunc, input, renderBody, options) {
        if (!input) {
            input = {};
        }

        var hasOutParam;
        var targetProperty;
        var parent;
        var hasNestedTags;
        var isRepeated;

        if (options) {
            hasOutParam = options.hasOutParam;
            parent = options.parent;
            targetProperty = options.targetProperty;
            hasNestedTags = options.hasNestedTags;
            isRepeated = options.isRepeated;
        }

        if (renderBody) {
            if (hasNestedTags) {
                renderBody(out, input);
            } else {
                input.renderBody = renderBody;
                input.invokeBody = function() {
                    if (!WARNED_INVOKE_BODY) {
                        WARNED_INVOKE_BODY = 1;
                        logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
                    }

                    if (!hasOutParam) {
                        var args = arrayFromArguments(arguments);
                        args.unshift(out);
                        renderBody.apply(this, args);
                    } else {
                        renderBody.apply(this, arguments);
                    }
                };
            }
        }

        if (renderFunc) {
            renderFunc(input, out);
        } else if (targetProperty) {
            if (isRepeated) {
                var existingArray = parent[targetProperty];
                if (existingArray) {
                    existingArray.push(input);
                } else {
                    parent[targetProperty] = [input];
                }
            } else {
                parent[targetProperty] = input;
            }
        }
    },
    /**
     * Internal helper method that captures the output of rendering.
     * This function works by swapping out the underlying writer to
     * a temporary writer that buffers a string. The provided function
     * is executed and the old writer is restored. Finally, the buffered
     * string is returned.
     */
    c: function (out, func) {
        var output = out.captureString(func);
        return {
            toString: function () {
                return output;
            }
        };
    },
    /**
     * Internal method to handle includes/partials
     * @private
     */
    i: function(out, path, data) {
        if (!path) {
            return;
        }

        if (data.body) {
            data.invokeBody = function() {
                if (!WARNED_INVOKE_BODY) {
                    WARNED_INVOKE_BODY = 1;
                    logger.warn('data.invokeBody() deprecated. Use data.body instead.', new Error().stack);
                }
                return data.body;
            };
        }

        if (typeof path === 'string') {
            runtime.render(path, data, out);
        } else if (typeof path.render === 'function') {
            path.render(data, out);
        } else {
            throw new Error('Invalid template');
        }

        return this;
    },
    /**
     * Internal helper method to do a shallow copy of the properties of one object to another.
     * @private
     */
    xt: extend
};
<commit_msg>Better resolving of tag renderer<commit_after>/*
* Copyright 2011 eBay Software Foundation
*
* 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.
*/

'use strict';
var escapeXml = require('raptor-util/escapeXml');
var escapeXmlAttr = escapeXml.attr;
var runtime = require('./'); // Circular dependency, but that is okay
var extend = require('raptor-util/extend');
var attr = require('raptor-util/attr');
var attrs = require('raptor-util/attrs');
var forEach = require('raptor-util/forEach');
var markoRegExp = /.html|\.marko(.xml|.html)?$/;
var arrayFromArguments = require('raptor-util/arrayFromArguments');
var logger = require('raptor-logging').logger(module);

var viewEngine;
var req = require;

try {
    viewEngine = req('view-engine');
} catch(e) {}

function notEmpty(o) {
    if (o == null) {
        return false;
    } else if (Array.isArray(o)) {
        return !!o.length;
    } else if (o === '') {
        return false;
    }

    return true;
}

function createDeferredRenderer(handler) {
    function deferredRenderer(input, out) {
        deferredRenderer.renderer(input, out);
    }

    // This is the initial function that will do the rendering. We replace
    // the renderer with the actual renderer func on the first render
    deferredRenderer.renderer = function(input, out) {
        var rendererFunc = handler.renderer || handler.render;
        if (typeof renderFunc !== 'function') {
            throw new Error('Invalid tag handler: ' + handler);
        }
        // Use the actual renderer from now on
        deferredRenderer.renderer = rendererFunc;
        rendererFunc(input, out);
    };

    return deferredRenderer;
}

var WARNED_INVOKE_BODY = 0;

module.exports = {
    /**
     * Internal helper method to prevent null/undefined from being written out
     * when writing text that resolves to null/undefined
     * @private
     */
    s: function(str) {
        return (str == null) ? '' : str;
    },
    /**
     * Internal helper method to handle loops with a status variable
     * @private
     */
    fv: function (array, callback) {
        if (!array) {
            return;
        }
        if (!array.forEach) {
            array = [array];
        }
        var i = 0;
        var len = array.length;
        var loopStatus = {
                getLength: function () {
                    return len;
                },
                isLast: function () {
                    return i === len - 1;
                },
                isFirst: function () {
                    return i === 0;
                },
                getIndex: function () {
                    return i;
                }
            };
        for (; i < len; i++) {
            var o = array[i];
            callback(o || '', loopStatus);
        }
    },
    /**
     * Internal helper method to handle loops without a status variable
     * @private
     */
    f: forEach,
    /**
     * Internal helper method to handle native for loops
     * @private
     */
    fl: function (array, func) {
        if (array != null) {
            if (!Array.isArray(array)) {
                array = [array];
            }
            func(array, 0, array.length);
        }
    },
    /**
     * Internal helper method for looping over the properties of any object
     * @private
     */
    fp: function (o, func) {
        if (!o) {
            return;
        }
        for (var k in o) {
            if (o.hasOwnProperty(k)) {
                func(k, o[k]);
            }
        }
    },
    /**
     * Internal method to check if an object/array is empty
     * @private
     */
    e: function (o) {
        return !notEmpty(o);
    },
    /**
     * Internal method to check if an object/array is not empty
     * @private
     */
    ne: notEmpty,
    /**
     * Internal method to escape special XML characters
     * @private
     */
    x: escapeXml,
    /**
     * Internal method to escape special XML characters within an attribute
     * @private
     */
    xa: escapeXmlAttr,
    /**
     * Internal helper to prevent an object from being escaped
     * @private
     */
    nx: function (str) {
        return {
            toString: function () {
                return str;
            }
        };
    },
    /**
     * Internal method to render a single HTML attribute
     * @private
     */
    a: attr,

    /**
     * Internal method to render multiple HTML attributes based on the properties of an object
     * @private
     */
    as: attrs,
    /**
     * Loads a template
     */
    l: function(path, req) {
        if (typeof path === 'string') {
            if (path.charAt(0) === '.' && req.resolve) { // Check if the path is relative
                // The path is relative so use require.resolve to fully resolve the path
                path = req.resolve(path);
            }

            if (!viewEngine || markoRegExp.test(path)) {
                return runtime.load(path);
            } else {
                return viewEngine.load(path);
            }
        } else if (path.render) {
            // Assume it is already a pre-loaded template
            return path;
        } else {
            return runtime.load(path);
        }
    },
    /**
     * Returns the render function for a tag handler
     */
    r: function(handler) {
        if (typeof handler === 'function') {
            return handler;
        }

        var renderFunc = handler.renderer || handler.render;

        // If the user code has a circular function then the renderer function
        // may not be available on the module. Since we can't get a reference
        // to the actual renderer(input, out) function right now we lazily
        // try to get access to it later.
        if (typeof renderFunc !== 'function') {
            return createDeferredRenderer(handler);
        }

        return renderFunc;
    },

    // ----------------------------------
    // The helpers listed below require an out
    // ----------------------------------

    /**
     * Invoke a tag handler render function
     */
    t: function (out, renderFunc, input, renderBody, options) {
        if (!input) {
            input = {};
        }

        var hasOutParam;
        var targetProperty;
        var parent;
        var hasNestedTags;
        var isRepeated;

        if (options) {
            hasOutParam = options.hasOutParam;
            parent = options.parent;
            targetProperty = options.targetProperty;
            hasNestedTags = options.hasNestedTags;
            isRepeated = options.isRepeated;
        }

        if (renderBody) {
            if (hasNestedTags) {
                renderBody(out, input);
            } else {
                input.renderBody = renderBody;
                input.invokeBody = function() {
                    if (!WARNED_INVOKE_BODY) {
                        WARNED_INVOKE_BODY = 1;
                        logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
                    }

                    if (!hasOutParam) {
                        var args = arrayFromArguments(arguments);
                        args.unshift(out);
                        renderBody.apply(this, args);
                    } else {
                        renderBody.apply(this, arguments);
                    }
                };
            }
        }

        if (renderFunc) {
            renderFunc(input, out);
        } else if (targetProperty) {
            if (isRepeated) {
                var existingArray = parent[targetProperty];
                if (existingArray) {
                    existingArray.push(input);
                } else {
                    parent[targetProperty] = [input];
                }
            } else {
                parent[targetProperty] = input;
            }
        }
    },
    /**
     * Internal helper method that captures the output of rendering.
     * This function works by swapping out the underlying writer to
     * a temporary writer that buffers a string. The provided function
     * is executed and the old writer is restored. Finally, the buffered
     * string is returned.
     */
    c: function (out, func) {
        var output = out.captureString(func);
        return {
            toString: function () {
                return output;
            }
        };
    },
    /**
     * Internal method to handle includes/partials
     * @private
     */
    i: function(out, path, data) {
        if (!path) {
            return;
        }

        if (data.body) {
            data.invokeBody = function() {
                if (!WARNED_INVOKE_BODY) {
                    WARNED_INVOKE_BODY = 1;
                    logger.warn('data.invokeBody() deprecated. Use data.body instead.', new Error().stack);
                }
                return data.body;
            };
        }

        if (typeof path === 'string') {
            runtime.render(path, data, out);
        } else if (typeof path.render === 'function') {
            path.render(data, out);
        } else {
            throw new Error('Invalid template');
        }

        return this;
    },
    /**
     * Internal helper method to do a shallow copy of the properties of one object to another.
     * @private
     */
    xt: extend
};
<|endoftext|>


<commit_before>(function(){
    var Event = function(evt){
        try{
            this.name = this.computeName(evt.payload);
            this.values = this.getValues(evt.payload);
            this.url = this.computeUrl(evt.payload);
        }catch(e){
            console.log(e.message + "for " + JSON.stringify(evt));
        }
    };

    Event.prototype.computeName = function(evt){
        return evt.objectTags.join(' ') + ' ' +
            evt.actionTags.join(' ');
    };

    Event.prototype.getValues = function(evt){
        var properties = JSON.stringify(evt.properties),
        value = properties
            .replace("{", "")
            .replace("}","")
            .replace(/"/g, '');

        return value;
    };

    Event.prototype.humanizedValue = function(){
        var value = "";

        //dirty hack for "duration"
        if("duration" == this.values.split(':')[0]){
            var durationString = this.values.split(":")[1];
            var duration = moment.duration(parseInt(durationString) * 1000),
            humanized_value = "";

            humanized_value += duration.hours() ? duration.hours() + " hour " : "";
            humanized_value += duration.minutes() ? duration.minutes() + " mins " : "";
            humanized_value += duration.seconds() ? duration.seconds() + " secs " : "";

            value = humanized_value;
        }else{
            var valueString = this.values.split(",")[0],
            valueTitle = valueString.split(":")[0],
            valueCount = parseFloat(valueString.split(":")[1]).toFixed(1);

            value = valueCount + " " + valueTitle.replace(/-/g, " ");
        }

        return value;
    }

    Event.prototype.computeUrl = function(evt){
        //sum on first existing event FOR NOW
        var first_value = this.values.split(":")[0],
        operation;

        //hack for noiseapp (for now)
        if("dba" == operation){
            operation = "mean(dba)";
        }else{
            operation = "sum(" + first_value  + ")";
        }

        return "window.location.href = " +
            "'/v1/users/" + username + "/events/" + evt.objectTags.join(',') +
            "/" + evt.actionTags.join(',') + "/" + operation +
            "/daily/barchart'";
    };

    window.OsEvent = Event;
})();
<commit_msg>Timeline display string if its NaN<commit_after>(function(){
    var Event = function(evt){
        try{
            this.name = this.computeName(evt.payload);
            this.values = this.getValues(evt.payload);
            this.url = this.computeUrl(evt.payload);
        }catch(e){
            console.log(e.message + "for " + JSON.stringify(evt));
        }
    };

    Event.prototype.computeName = function(evt){
        return evt.objectTags.join(' ') + ' ' +
            evt.actionTags.join(' ');
    };

    Event.prototype.getValues = function(evt){
        var properties = JSON.stringify(evt.properties),
        value = properties
            .replace("{", "")
            .replace("}","")
            .replace(/"/g, '');

        return value;
    };

    Event.prototype.humanizedValue = function(){
        var value = "";

        //dirty hack for "duration"
        if("duration" == this.values.split(':')[0]){
            var durationString = this.values.split(":")[1];
            var duration = moment.duration(parseInt(durationString) * 1000),
            humanized_value = "";

            humanized_value += duration.hours() ? duration.hours() + " hour " : "";
            humanized_value += duration.minutes() ? duration.minutes() + " mins " : "";
            humanized_value += duration.seconds() ? duration.seconds() + " secs " : "";

            value = humanized_value;
        }else{
            var valueString = this.values.split(",")[0],
            valueTitle = valueString.split(":")[0],
            valueCount = parseFloat(valueString.split(":")[1]).toFixed(1);

            //hack, if its not a number just display the string as it is.
            if(isNaN(valueCount)){
                valueCount = valueString.split(":")[1];
            }

            value = valueCount + " " + valueTitle.replace(/-/g, " ");
        }

        return value;
    }

    Event.prototype.computeUrl = function(evt){
        //sum on first existing event FOR NOW
        var first_value = this.values.split(":")[0],
        operation;

        //hack for noiseapp (for now)
        if("dba" == operation){
            operation = "mean(dba)";
        }else{
            operation = "sum(" + first_value  + ")";
        }

        return "window.location.href = " +
            "'/v1/users/" + username + "/events/" + evt.objectTags.join(',') +
            "/" + evt.actionTags.join(',') + "/" + operation +
            "/daily/barchart'";
    };

    window.OsEvent = Event;
})();
<|endoftext|>