<commit_before>(function(App) {

  'use strict';

  App.View = App.View || {};

  App.View.Filters = Backbone.View.extend({

    el: '.c-filters',

    events: {
      'click .js-filters-maximizer' : '_onClickToggleFilters'
    },

    options: {
      isFixed: false,
      showFiltersClass: "-show-filters",
      openFiltersAnimateSpeed: 0.5,
      filterTriggerClass: ".js-filter",
      fixedClass: "-fixed",
    },

    initialize: function(settings) {
      var opts = settings && settings.options ? settings.options : {};
      this.options = _.extend({}, this.options, opts);
      this.staticParams = typeof settings.staticParams !== "undefined" ? settings.staticParams : null;

      this._cache();
      this._loadFilters();
      this._setSelectFilters();
      this._loadStickyEvent();
    },

    _cache: function() {
      this.opened = this.$el.hasClass(this.options.showFiltersClass);
      this.$container = this.$el.find('.container');
      if ( this.$container.length > 0 ) {
        this.offsetTop = this.$el.offset().top + 6;
      }
    },

    _loadFilters: function() {
      this.filters = [];
      _.each(this.$el.find(this.options.filterTriggerClass), function(item) {
        var filter = $(item);
        switch (filter.data("filter-type")) {
          case "select":
            this._loadSelect(this._getHelperFilterObject(filter));
            break;
          case "tags":
            this._loadTag(this._getHelperFilterObject(filter));
            break;
          case "menu":
            this._loadMenu(this._getHelperFilterObject(filter));
            break;
        }
      }.bind(this));
    },

    _loadSelect: function(helperFilter) {
      this.filters.push(new App.Helper.SelectHandler(helperFilter));
    },

    _loadTag: function(helperFilter) {
      this.filters.push(new App.Helper.TagSelectHandler(helperFilter));
    },

    _loadMenu: function(helperFilter) {
      this.filters.push(new App.Helper.MenuFilter(helperFilter));
    },

    _getHelperFilterObject: function(filter) {
      return {
        el: filter,
        options: {
          callback: this._filterMedia.bind(this),
          closeAllFilters: this._closeAllFilters.bind(this)
        }
      };
    },

    _filterMedia: function(data) {
      this._trackFilter(data);
      this._setHash();
    },

    _setHash: function () {
      var url = '?' + this.getFilterValues();
      Turbolinks.visit(url, {})
    },

    _onClickToggleFilters: function() {
      if (!this.opened) {
        if (typeof this.filtersHeight == "undefined") {
          this.filtersHeight = this._getFiltersHeight();
        }
        this.$container.css("height", this.filtersHeight);
      } else {
        this.$container.attr("style", "");
      }

      this.opened = !this.opened;
      this.$el.toggleClass(this.options.showFiltersClass);
    },

    _getFiltersHeight: function() {
      var elem = this.$container.clone().css("height", "auto").appendTo(this.$el);
      var height = elem.css("height");
      elem.remove();

      return height;
    },

    getFilterValues: function() {
      var queryStr = '';
      _.each(this.filters, function (filter) {
        if ( filter.selectedValues.length > 0 ) {
          if ( queryStr !== '' ) {
            queryStr += '&';
          }
          queryStr += filter.key + '=';
          _.each(filter.selectedValues.sort(), function (value, index) {
            queryStr += value;
            if ( index < filter.selectedValues.length - 1 ) {
              queryStr += ',';
            }
          });
        }
      });
      if (this.staticParams !== null) {
        queryStr += this.staticParams;
      }

      return queryStr;
    },

    _getFiltersFromUrl: function() {
      var vars = {}, hash;
      var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
      var route = 'http://' + window.location.host + window.location.pathname;
      if ( hashes[0] === route || hashes[0] === "") {
        return false
      }
      for ( var i = 0; i < hashes.length; i++ ) {
          hash = hashes[i].split('=');
          vars[hash[0]] = hash[1];
      }
      return vars;
    },

    _setSelectFilters: function() {
      var activeFilters = this._getFiltersFromUrl();
      if ( activeFilters === false ) {
        return false
      }
      _.each(activeFilters, function(value, filter) {
        var selectedValues = value.split(",")
        if(filter !== 'media') {
          selectedValues = selectedValues.map(Number);
        }
        var filter = _.findWhere(this.filters, {key: filter});
        if(typeof filter != "undefined") {
          filter.selectedValues = selectedValues;
        }
      }.bind(this));
    },

    _loadStickyEvent: function () {
      $(window).scroll(function () {
        var scrollTop = $(window).scrollTop();

        if(!this.options.isFixed && scrollTop >= this.offsetTop) {
          this.$el.addClass(this.options.fixedClass);
          this.options.isFixed = true;
        } else if(this.options.isFixed && scrollTop < this.offsetTop) {
          this.$el.removeClass(this.options.fixedClass);
          this.options.isFixed = false;
        }
      }.bind(this));
    },

    _closeAllFilters: function () {
      _.each(this.filters, function (filter) {
        if(filter.options.isOpen) {
          filter.closeProcess();
        }
      });
    },

    _trackFilter: function (data) {
      if(Array.isArray(data.value) && data.value.length === 0) {
        data.value = null;
      } else if(data.value !== null && typeof data.value === "object") {
        data.value = JSON.stringify(data.value);
      }

      ga('send', 'event', this.options.trackLabel, data.value, data.key);
    }

  });

})(this.App);
<commit_msg>removes protocol<commit_after>(function(App) {

  'use strict';

  App.View = App.View || {};

  App.View.Filters = Backbone.View.extend({

    el: '.c-filters',

    events: {
      'click .js-filters-maximizer' : '_onClickToggleFilters'
    },

    options: {
      isFixed: false,
      showFiltersClass: "-show-filters",
      openFiltersAnimateSpeed: 0.5,
      filterTriggerClass: ".js-filter",
      fixedClass: "-fixed",
    },

    initialize: function(settings) {
      var opts = settings && settings.options ? settings.options : {};
      this.options = _.extend({}, this.options, opts);
      this.staticParams = typeof settings.staticParams !== "undefined" ? settings.staticParams : null;

      this._cache();
      this._loadFilters();
      this._setSelectFilters();
      this._loadStickyEvent();
    },

    _cache: function() {
      this.opened = this.$el.hasClass(this.options.showFiltersClass);
      this.$container = this.$el.find('.container');
      if ( this.$container.length > 0 ) {
        this.offsetTop = this.$el.offset().top + 6;
      }
    },

    _loadFilters: function() {
      this.filters = [];
      _.each(this.$el.find(this.options.filterTriggerClass), function(item) {
        var filter = $(item);
        switch (filter.data("filter-type")) {
          case "select":
            this._loadSelect(this._getHelperFilterObject(filter));
            break;
          case "tags":
            this._loadTag(this._getHelperFilterObject(filter));
            break;
          case "menu":
            this._loadMenu(this._getHelperFilterObject(filter));
            break;
        }
      }.bind(this));
    },

    _loadSelect: function(helperFilter) {
      this.filters.push(new App.Helper.SelectHandler(helperFilter));
    },

    _loadTag: function(helperFilter) {
      this.filters.push(new App.Helper.TagSelectHandler(helperFilter));
    },

    _loadMenu: function(helperFilter) {
      this.filters.push(new App.Helper.MenuFilter(helperFilter));
    },

    _getHelperFilterObject: function(filter) {
      return {
        el: filter,
        options: {
          callback: this._filterMedia.bind(this),
          closeAllFilters: this._closeAllFilters.bind(this)
        }
      };
    },

    _filterMedia: function(data) {
      this._trackFilter(data);
      this._setHash();
    },

    _setHash: function () {
      var url = '?' + this.getFilterValues();
      Turbolinks.visit(url, {})
    },

    _onClickToggleFilters: function() {
      if (!this.opened) {
        if (typeof this.filtersHeight == "undefined") {
          this.filtersHeight = this._getFiltersHeight();
        }
        this.$container.css("height", this.filtersHeight);
      } else {
        this.$container.attr("style", "");
      }

      this.opened = !this.opened;
      this.$el.toggleClass(this.options.showFiltersClass);
    },

    _getFiltersHeight: function() {
      var elem = this.$container.clone().css("height", "auto").appendTo(this.$el);
      var height = elem.css("height");
      elem.remove();

      return height;
    },

    getFilterValues: function() {
      var queryStr = '';
      _.each(this.filters, function (filter) {
        if ( filter.selectedValues.length > 0 ) {
          if ( queryStr !== '' ) {
            queryStr += '&';
          }
          queryStr += filter.key + '=';
          _.each(filter.selectedValues.sort(), function (value, index) {
            queryStr += value;
            if ( index < filter.selectedValues.length - 1 ) {
              queryStr += ',';
            }
          });
        }
      });
      if (this.staticParams !== null) {
        queryStr += this.staticParams;
      }

      return queryStr;
    },

    _getFiltersFromUrl: function() {
      var vars = {}, hash;
      var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
      var route = '//' + window.location.host + window.location.pathname;
      if ( hashes[0] === route || hashes[0] === "") {
        return false
      }
      for ( var i = 0; i < hashes.length; i++ ) {
          hash = hashes[i].split('=');
          vars[hash[0]] = hash[1];
      }
      return vars;
    },

    _setSelectFilters: function() {
      var activeFilters = this._getFiltersFromUrl();
      if ( activeFilters === false ) {
        return false
      }
      _.each(activeFilters, function(value, filter) {
        var selectedValues = value.split(",")
        if(filter !== 'media') {
          selectedValues = selectedValues.map(Number);
        }
        var filter = _.findWhere(this.filters, {key: filter});
        if(typeof filter != "undefined") {
          filter.selectedValues = selectedValues;
        }
      }.bind(this));
    },

    _loadStickyEvent: function () {
      $(window).scroll(function () {
        var scrollTop = $(window).scrollTop();

        if(!this.options.isFixed && scrollTop >= this.offsetTop) {
          this.$el.addClass(this.options.fixedClass);
          this.options.isFixed = true;
        } else if(this.options.isFixed && scrollTop < this.offsetTop) {
          this.$el.removeClass(this.options.fixedClass);
          this.options.isFixed = false;
        }
      }.bind(this));
    },

    _closeAllFilters: function () {
      _.each(this.filters, function (filter) {
        if(filter.options.isOpen) {
          filter.closeProcess();
        }
      });
    },

    _trackFilter: function (data) {
      if(Array.isArray(data.value) && data.value.length === 0) {
        data.value = null;
      } else if(data.value !== null && typeof data.value === "object") {
        data.value = JSON.stringify(data.value);
      }

      ga('send', 'event', this.options.trackLabel, data.value, data.key);
    }

  });

})(this.App);
<|endoftext|>


<commit_before>// @flow

import * as React from 'react';
import PropTypes from 'prop-types';
import Relay from 'react-relay/classic';
import { parse } from 'query-string';
import unique from 'array-unique';

import Button from 'app/components/shared/Button';
import CollapsableArea from 'app/components/shared/CollapsableArea';
import Dialog from 'app/components/shared/Dialog';
import FormTextField from 'app/components/shared/FormTextField';
import FormTextarea from 'app/components/shared/FormTextarea';
import FormDataList from 'app/components/shared/FormDataList';

type Props = {
  pipeline: Object,
  build: ?Object,
  isOpen: ?boolean,
  onRequestClose: Function
};

type State = {
  showingOptions: boolean,
  creatingBuild: boolean,
  defaultValues: {
    // putting these here so it's obvious they exist(!)
    message: ?string,
    commit: ?string,
    branch: ?string,
    env: ?string,
    clean_checkout: ?boolean
  }
};

class CreateBuildDialog extends React.PureComponent<Props, State> {
  static propTypes = {
    pipeline: PropTypes.object.isRequired,
    isOpen: PropTypes.bool,
    onRequestClose: PropTypes.func
  };

  state = {
    showingOptions: false,
    creatingBuild: false,
    defaultValues: {
      // putting these here so it's obvious they exist(!)
      message: undefined,
      commit: undefined,
      branch: undefined,
      env: undefined,
      clean_checkout: undefined
    }
  };

  form: ?HTMLFormElement;
  buildMessageTextField: FormTextField;
  buildCommitTextField: FormTextField;
  buildBranchTextField: FormTextField;

  componentWillMount() {
    const [hashPath, hashQuery] = window.location.hash.split('?');

    if (hashPath === '#new') {
      const newState = {};

      newState.defaultValues = parse(hashQuery);

      // expand expando area if values in expando area are non-default!
      if (newState.defaultValues.env || newState.defaultValues.clean_checkout) {
        newState.showingOptions = true;
      }

      this.setState(newState);
    }
  }

  componentDidMount() {
    // Focus the build message input box if the dialog started life opened.
    if (this.props.isOpen && this.buildMessageTextField) {
      this.buildMessageTextField.focus();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.isOpen && this.props.isOpen) {
      // Focus the build message input box if the dialog was just opened. This
      // is a total hack since when this component transitions from `isOpen
      // (false => true)` the buildMessageTextField doesn't actually exist yet.
      //
      // So we'll do this hacky thing here, let this component render, then
      // focus the text field if we've got it.
      setTimeout(() => {
        if (this.buildMessageTextField) {
          this.buildMessageTextField.focus();
        }
      }, 0);
    }
  }

  render() {
    const branchSuggestions = [this.props.pipeline.defaultBranch];
    const commitSuggestions = [this.props.pipeline.defaultCommit];

    // Add suggestions from the current build. This code is weird because Flow
    // is weird...
    if (this.props.build && this.props.build.branch) {
      branchSuggestions.push(this.props.build.branch);
    }
    if (this.props.build && this.props.build.commit) {
      commitSuggestions.push(this.props.build.commit);
    }

    // Add suggestions from the URL
    if (window.location.search) {
      const queryParams = parse(window.location.search);
      if (queryParams["branch"]) {
        branchSuggestions.push(queryParams["branch"]);
      }
      if (queryParams["commit"]) {
        commitSuggestions.push(queryParams["commit"]);
      }
    }

    return (
      <Dialog isOpen={this.props.isOpen} onRequestClose={this.props.onRequestClose} width={400}>
        <form
          action={`/organizations/${this.props.pipeline.organization.slug}/pipelines/${this.props.pipeline.slug}/builds`}
          acceptCharset=""
          method="POST"
          ref={(form) => this.form = form}
        >

          <input type="hidden" name="utf8" value="✓" />
          <input type="hidden" name={window._csrf.param} value={window._csrf.token} />

New Build

            <FormTextField
              name="build[message]"
              label="Message"
              help="Description of the build. If left blank, the commit message will be used once the build starts"
              defaultValue={this.state.defaultValues.message || this.props.pipeline.defaultMessage}
              ref={(tf) => this.buildMessageTextField = tf}
            />

            <FormDataList
              id="new-build-commit-suggestions"
              values={unique(commitSuggestions)}
            />

            <FormTextField
              name="build[commit]"
              label="Commit"
              list="new-build-commit-suggestions"
              defaultValue={this.state.defaultValues.commit || this.props.pipeline.defaultCommit}
              ref={(tf) => this.buildCommitTextField = tf}
            />

            <FormDataList
              id="new-build-branch-suggestions"
              values={unique(branchSuggestions)}
            />

            <FormTextField
              name="build[branch]"
              label="Branch"
              list="new-build-branch-suggestions"
              defaultValue={this.state.defaultValues.branch || this.props.pipeline.defaultBranch}
              ref={(tf) => this.buildBranchTextField = tf}
            />

            <CollapsableArea
              label="Options"
              maxHeight={250}
              onToggle={this.handleOptionsToggle}
              collapsed={!this.state.showingOptions}
            >
              <FormTextarea
                name="build[env]"
                label="Environment Variables"
                help={Place each environment variable on a new line, in the format KEY=value}
                rows={3}
                defaultValue={this.state.defaultValues.env}
                tabIndex={this.state.showingOptions ? 0 : -1}
              />

                <input type="hidden" name="build[clean_checkout]" value="0" />

                  <input
                    className="absolute"
                    name="build[clean_checkout]"
                    type="checkbox"
                    value="1"
                    defaultChecked={this.state.defaultValues.clean_checkout === 'true'}
                    tabIndex={this.state.showingOptions ? 0 : -1}
                  />
                  {' '}
                  Clean checkout

Force the agent to remove any existing build directory and perform a fresh checkout

            </CollapsableArea>

            <Button className="col-12" onClick={this.handleCreateBuildButtonClick} loading={this.state.creatingBuild ? "Creating Build..." : false}>Create Build</Button>

        </form>
      </Dialog>
    );
  }

  handleCreateBuildButtonClick = (event) => {
    event.preventDefault();

    this.form && this.form.submit();
  }

  handleOptionsToggle = () => {
    this.setState({ showingOptions: !this.state.showingOptions });
  }
}

export default Relay.createContainer(CreateBuildDialog, {
  fragments: {
    build: () => Relay.QL`
      fragment on Build {
        commit
        branch
      }
    `,
    pipeline: () => Relay.QL`
      fragment on Pipeline {
        slug
        defaultMessage
        defaultCommit
        defaultBranch
        organization {
          slug
        }
      }
    `
  }
});
<commit_msg>Form validation for the "New Build" dialog, but just for commit and branch<commit_after>// @flow

import * as React from 'react';
import PropTypes from 'prop-types';
import Relay from 'react-relay/classic';
import { parse } from 'query-string';
import unique from 'array-unique';

import Button from 'app/components/shared/Button';
import CollapsableArea from 'app/components/shared/CollapsableArea';
import Dialog from 'app/components/shared/Dialog';
import FormTextField from 'app/components/shared/FormTextField';
import FormTextarea from 'app/components/shared/FormTextarea';
import FormDataList from 'app/components/shared/FormDataList';

type Props = {
  pipeline: Object,
  build: ?Object,
  isOpen: ?boolean,
  onRequestClose: Function
};

type State = {
  showingOptions: boolean,
  creatingBuild: boolean,
  defaultValues: {
    // putting these here so it's obvious they exist(!)
    message: ?string,
    commit: ?string,
    branch: ?string,
    env: ?string,
    clean_checkout: ?boolean
  }
};

class CreateBuildDialog extends React.PureComponent<Props, State> {
  static propTypes = {
    pipeline: PropTypes.object.isRequired,
    isOpen: PropTypes.bool,
    onRequestClose: PropTypes.func
  };

  state = {
    showingOptions: false,
    creatingBuild: false,
    defaultValues: {
      // putting these here so it's obvious they exist(!)
      message: undefined,
      commit: undefined,
      branch: undefined,
      env: undefined,
      clean_checkout: undefined
    }
  };

  form: ?HTMLFormElement;
  buildMessageTextField: FormTextField;
  buildCommitTextField: FormTextField;
  buildBranchTextField: FormTextField;

  componentWillMount() {
    const [hashPath, hashQuery] = window.location.hash.split('?');

    if (hashPath === '#new') {
      const newState = {};

      newState.defaultValues = parse(hashQuery);

      // expand expando area if values in expando area are non-default!
      if (newState.defaultValues.env || newState.defaultValues.clean_checkout) {
        newState.showingOptions = true;
      }

      this.setState(newState);
    }
  }

  componentDidMount() {
    // Focus the build message input box if the dialog started life opened.
    if (this.props.isOpen && this.buildMessageTextField) {
      this.buildMessageTextField.focus();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.isOpen && this.props.isOpen) {
      // Focus the build message input box if the dialog was just opened. This
      // is a total hack since when this component transitions from `isOpen
      // (false => true)` the buildMessageTextField doesn't actually exist yet.
      //
      // So we'll do this hacky thing here, let this component render, then
      // focus the text field if we've got it.
      setTimeout(() => {
        if (this.buildMessageTextField) {
          this.buildMessageTextField.focus();
        }
      }, 0);
    }
  }

  render() {
    const branchSuggestions = [this.props.pipeline.defaultBranch];
    const commitSuggestions = [this.props.pipeline.defaultCommit];

    // Add suggestions from the current build. This code is weird because Flow
    // is weird...
    if (this.props.build && this.props.build.branch) {
      branchSuggestions.push(this.props.build.branch);
    }
    if (this.props.build && this.props.build.commit) {
      commitSuggestions.push(this.props.build.commit);
    }

    // Add suggestions from the URL
    if (window.location.search) {
      const queryParams = parse(window.location.search);
      if (queryParams["branch"]) {
        branchSuggestions.push(queryParams["branch"]);
      }
      if (queryParams["commit"]) {
        commitSuggestions.push(queryParams["commit"]);
      }
    }

    return (
      <Dialog isOpen={this.props.isOpen} onRequestClose={this.props.onRequestClose} width={400}>
        <form
          action={`/organizations/${this.props.pipeline.organization.slug}/pipelines/${this.props.pipeline.slug}/builds`}
          acceptCharset=""
          method="POST"
          ref={(form) => this.form = form}
        >

          <input type="hidden" name="utf8" value="✓" />
          <input type="hidden" name={window._csrf.param} value={window._csrf.token} />

New Build

            <FormTextField
              name="build[message]"
              label="Message"
              help="Description of the build. If left blank, the commit message will be used once the build starts"
              defaultValue={this.state.defaultValues.message || this.props.pipeline.defaultMessage}
              ref={(tf) => this.buildMessageTextField = tf}
            />

            <FormDataList
              id="new-build-commit-suggestions"
              values={unique(commitSuggestions)}
            />

            <FormTextField
              name="build[commit]"
              label="Commit"
              list="new-build-commit-suggestions"
              required={true}
              defaultValue={this.state.defaultValues.commit || this.props.pipeline.defaultCommit}
              ref={(tf) => this.buildCommitTextField = tf}
            />

            <FormDataList
              id="new-build-branch-suggestions"
              values={unique(branchSuggestions)}
            />

            <FormTextField
              name="build[branch]"
              label="Branch"
              list="new-build-branch-suggestions"
              required={true}
              defaultValue={this.state.defaultValues.branch || this.props.pipeline.defaultBranch}
              ref={(tf) => this.buildBranchTextField = tf}
            />

            <CollapsableArea
              label="Options"
              maxHeight={250}
              onToggle={this.handleOptionsToggle}
              collapsed={!this.state.showingOptions}
            >
              <FormTextarea
                name="build[env]"
                label="Environment Variables"
                help={Place each environment variable on a new line, in the format KEY=value}
                rows={3}
                defaultValue={this.state.defaultValues.env}
                tabIndex={this.state.showingOptions ? 0 : -1}
              />

                <input type="hidden" name="build[clean_checkout]" value="0" />

                  <input
                    className="absolute"
                    name="build[clean_checkout]"
                    type="checkbox"
                    value="1"
                    defaultChecked={this.state.defaultValues.clean_checkout === 'true'}
                    tabIndex={this.state.showingOptions ? 0 : -1}
                  />
                  {' '}
                  Clean checkout

Force the agent to remove any existing build directory and perform a fresh checkout

            </CollapsableArea>

            <Button className="col-12" onClick={this.handleCreateBuildButtonClick} loading={this.state.creatingBuild ? "Creating Build..." : false}>Create Build</Button>

        </form>
      </Dialog>
    );
  }

  handleCreateBuildButtonClick = (event) => {
    event.preventDefault();

    if (this.isValid()) {
      this.setState({ creatingBuild: true });
      this.form && this.form.submit();
    }
  }

  isValid() {
    // Ideally these required fields should prevent themselves from being
    // submitted… but somehow they don't?
    if (!this.buildCommitTextField.value) {
      this.buildCommitTextField.focus();
      return false;
    }
    if (!this.buildBranchTextField.value) {
      this.buildBranchTextField.focus();
      return false;
    }

    return true;
  }

  handleOptionsToggle = () => {
    this.setState({ showingOptions: !this.state.showingOptions });
  }
}

export default Relay.createContainer(CreateBuildDialog, {
  fragments: {
    build: () => Relay.QL`
      fragment on Build {
        commit
        branch
      }
    `,
    pipeline: () => Relay.QL`
      fragment on Pipeline {
        slug
        defaultMessage
        defaultCommit
        defaultBranch
        organization {
          slug
        }
      }
    `
  }
});
<|endoftext|>


<commit_before>////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Configuration...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const BACKGROUND_COLOR = '#eaeaea';
const BORDER_COLOR = '#e4e4e4';
const HIGHLIGHT_COLOR = '#f1f1f1';
const BORDER_WIDTH = 4;

const WALL_COLOR_DARK = '#333333';
const WALL_COLOR_MEDIUM = '#555555';
const WALL_COLOR_LIGHT = '#777777';
const WALL_THICKNESS = 3;

const GRID_COLOR = '#';
const GRID_WIDTH = 2;
const GRID_SPACING = 24;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Object definitions...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Something in the world is drawable, so everything should inherit from this.
 */
function Drawable() {

}

Drawable.prototype = {
    draw: function (ctx, offX, offY) {
        alert("Inherit the draw function...");
    },
    mousemoveHandler: function () {
        alert("Inherit the mousemove function...");
    },
    mousedownHandler: function (evt) {
        alert("Inherit the mousedown function...");
    },
    mouseupHandler: function (evt) {
        alert("Inherit the mouseup function...")
    }
};

/**
 * A room is a container/"layer" for walls and other household objects.
 */
function Room() {
    this.walls = [];
}
Room.prototype = Drawable;
Room.prototype.constructor = Room;

/**
 * A wall has a start and endpoint, and fill in-between.
 */
function Wall(colA, rowA, colB, rowB) {
    this.endpointA = {c: colA, r: rowA, moving: false};
    this.endpointB = {c: colB, r: rowB, moving: false};
    this.draw = function (ctx, offX, offY) {
        // Upper-left corners of each endpoint.
        var aX = this.endpointA.c * GRID_SPACING - offX;
        var aY = this.endpointA.r * GRID_SPACING - offY;
        var bX = this.endpointB.c * GRID_SPACING - offX;
        var bY = this.endpointB.r * GRID_SPACING - offY;

        ctx.lineWidth = WALL_THICKNESS;

        // Draw a wall connecting the two endpoints. We want this wall to be of bounding thickness, so let's think about
        // this. The innermost and outermost pairs of verts won't have a line connecting them - we just want lines
        // between the middle-distance pairs of verts, to form the wall.
        ctx.fillStyle = WALL_COLOR_LIGHT;
        ctx.strokeStyle = WALL_COLOR_MEDIUM;

        // Find the quadrant that one endpoint is in relative to the other - this way we know which 2 verts need to be
        // connected with straight lines.
        if (aY > bY) {
            quadrant = aX > bX ? 1 : 2;
        } else {
            quadrant = aX > bX ? 4 : 3;
        }

        // Calculate middle endpoint positions.
        var mediumPoint1A = {x: 0, y: 0};
        var mediumPoint1B = {x: 0, y: 0};
        var mediumPoint2A = {x: 0, y: 0};
        var mediumPoint2B = {x: 0, y: 0};

        switch (quadrant) {
            case 1:
            case 3:
                mediumPoint1A.x = aX + GRID_SPACING;
                mediumPoint1A.y = aY;
                mediumPoint1B.x = bX + GRID_SPACING;
                mediumPoint1B.y = bY;

                mediumPoint2A.x = aX;
                mediumPoint2A.y = aY + GRID_SPACING;
                mediumPoint2B.x = bX;
                mediumPoint2B.y = bY + GRID_SPACING;
                break;
            case 2:
            case 4:
                mediumPoint1A.x = aX;
                mediumPoint1A.y = aY;
                mediumPoint1B.x = bX;
                mediumPoint1B.y = bY;

                mediumPoint2A.x = aX + GRID_SPACING;
                mediumPoint2A.y = aY + GRID_SPACING;
                mediumPoint2B.x = bX + GRID_SPACING;
                mediumPoint2B.y = bY + GRID_SPACING;
                break;
            default:

        }

        // Create the path between the medium points.
        ctx.beginPath();

        // Move to the first medium point, draw to its pair, draw to the second's pair, draw to the second..
        ctx.moveTo(mediumPoint1A.x, mediumPoint1A.y);
        ctx.lineTo(mediumPoint1B.x, mediumPoint1B.y);
        ctx.lineTo(mediumPoint2B.x, mediumPoint2B.y);
        ctx.lineTo(mediumPoint2A.x, mediumPoint2A.y);

        ctx.fill();
        ctx.stroke();

        // Draw handles on each endpoint.
        //TODO: Feedback fill if being dragged
        ctx.fillStyle = WALL_COLOR_LIGHT;
        ctx.strokeStyle = WALL_COLOR_DARK;
        ctx.strokeRect(aX, aY, GRID_SPACING, GRID_SPACING);
        ctx.strokeRect(bX, bY, GRID_SPACING, GRID_SPACING);

    };

    this.mousemoveHandler = function () {
        // Turn cursor if we're over a handle.
        if (mouseCol == this.endpointA.c && mouseRow == this.endpointA.r) {
            setCursorGrab();
        } else if (mouseCol == this.endpointB.c && mouseRow == this.endpointB.r) {
            setCursorGrab();
        }

        // If we're moving, keep the endpoint locations updated.
        if (this.endpointA.moving) {
            this.endpointA.r = mouseRow;
            this.endpointA.c = mouseCol;
        }
        if (this.endpointB.moving) {
            this.endpointB.r = mouseRow;
            this.endpointB.c = mouseCol;
        }
    };

    this.mousedownHandler = function (evt) {

        switch (evt.which) {
            case 1:
                // Check if there was a click in a handle - if so, set its moving property.
                if (mouseCol == this.endpointA.c && mouseRow == this.endpointA.r) {

                    this.endpointA.moving = true;
                } else if (mouseCol == this.endpointB.c && mouseRow == this.endpointB.r) {
                    this.endpointB.moving = true;
                }
                break;
            default:
                break;
        }
    };

    this.mouseupHandler = function (evt) {
        switch (evt.which) {
            case 1:
                // No matter where the click was, we are no longer moving.
                this.endpointA.moving = false;
                this.endpointB.moving = false;
                break;
            default:
                break;
        }
    };
}
Wall.prototype = Drawable;
Wall.prototype.constructor = Wall;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Instance variables...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Position - where is the viewport looking in absolute coordinates?
var worldX = 0;
var worldY = 0;

// Mouse position - x and y in viewport terms.
var mouseViewX = 0;
var mouseViewY = 0;

// Mouse row/col - in absolute coordinates.
var mouseCol = 0;
var mouseRow = 0;

// Mouse button states - true for down, false for up.
var mouse1 = false;
var mouse2 = false;
var mouse3 = false;

// All of the objects in the world.
var worldObjects = [];

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Canvas initialization...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var canvas = document.getElementById('floorplanCanvas');
var c = canvas.getContext('2d');

/**
 * Draw canvas elements.
 */
function redraw() {
    var width = canvas.clientWidth;
    var height = canvas.clientHeight;

    c.fillStyle = BACKGROUND_COLOR;
    c.fillRect(0, 0, width, height);

    // Draw a border around the canvas to make it look nice...
    c.strokeStyle = BORDER_COLOR;
    c.lineWidth = BORDER_WIDTH;
    c.strokeRect(0, 0, width, height);

    // Draw gridlines
    c.strokeStyle = GRID_COLOR;
    c.lineWidth = GRID_WIDTH;

    for (var i = 0; i < height; i += GRID_SPACING) {
        c.strokeRect(0, i - (worldY % GRID_SPACING), width, GRID_WIDTH);
    }

    for (var i = 0; i < width; i += GRID_SPACING) {
        c.strokeRect(i - (worldX % GRID_SPACING), 0, GRID_WIDTH, height);
    }

    // Draw all of the drawable objects.
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].draw(c, worldX, worldY);
    }
}

/**
 * Resolve the mouse position into row/col.
 */
function resolveMouse() {
    return {
        row: Math.floor((mouseViewY + worldY) / GRID_SPACING),
        col: Math.floor((mouseViewX + worldX) / GRID_SPACING)
    };
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Event listeners and initialization...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Keep canvas coordinate system syncrhonized with CSS coordinates on window resize.
 */
(function () {
    var canvas = $('#floorplanCanvas');

    initCanvas();
    function initCanvas() {
        window.addEventListener('resize', resizeCanvasWithWindow, false);
        resizeCanvasWithWindow();
    }

    function resizeCanvasWithWindow() {
        var w = $('#floorplanCanvasContainer').width();
        var h = $('#floorplanCanvasContainer').height();
        canvas.attr("width", w);
        canvas.attr("height", h);
        redraw();
    }
})();

/**
 * Want to track where the mouse moved to update, also implements scroll-dragging.
 */
function mouseMoveListener(e) {
    var offset = $('#floorplanCanvas')[0].getBoundingClientRect();

    // Calculate current mouse position
    var newX = e.clientX - offset.left;
    var newY = e.clientY - offset.top;

    // Calculate deltas
    var dX = newX - mouseViewX;
    var dY = newY - mouseViewY;

    // Update global mouse position
    mouseViewX = newX;
    mouseViewY = newY;

    // Move viewport if scrolling
    if (mouse3) {
        worldX -= dX;
        worldY -= dY;
    }

    // Calculate and update column and row locations
    var cr = resolveMouse();
    mouseCol = cr.col;
    mouseRow = cr.row;

    // Reset the cursor before letting objects change it.
    setCursorDefault();

    // Propogate mouse movement events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mousemoveHandler();
    }

    redraw();
}

/**
 * Watch for mousedown events and set global flags.
 */
function mouseDownListener(e) {
    switch (e.which) {
        case 1:
            mouse1 = true;
            break;
        case 2:
            mouse2 = true;
            break;
        case 3:
            mouse3 = true;
            break;
    }

    // Propogate mousedown events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mousedownHandler(e);
    }
}

/**
 * Watch for mouseup events and set global flags.
 */
function mouseUpListener(e) {
    switch (e.which) {
        case 1:
            mouse1 = false;
            break;
        case 2:
            mouse2 = false;
            break;
        case 3:
            mouse3 = false;
            break;
    }

    // Propogate mouseup events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mouseupHandler(e);
    }
}

// Register listeners
$('#floorplanCanvas').on('mousemove', mouseMoveListener);
$('#floorplanCanvas').on('mousedown', mouseDownListener);
$('#floorplanCanvas').on('mouseup', mouseUpListener);
$('#floorplanCanvas').on('contextmenu', function () {
    return false; // Disable context menu on canvas for RMB dragging
});

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sets the cursor to a grabby cursor on the canvas.
 */
function setCursorGrab() {
    $('#floorplanCanvas').css('cursor', 'move');
}

/**
 * Sets the cursor to the default cursor on the canvas.
 */
function setCursorDefault() {
    $('#floorplanCanvas').css('cursor', 'default');
}

//
// Debug functions
//

function addWall() {

    var w = new Wall(1, 1, 5, 1);

    worldObjects.push(w);
}<commit_msg>add canvas zooming<commit_after>// Configuration...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const BACKGROUND_COLOR = '#eaeaea';
const BORDER_COLOR = '#e4e4e4';
const HIGHLIGHT_COLOR = '#f1f1f1';
const BORDER_WIDTH = 4;

const WALL_COLOR_DARK = '#333333';
const WALL_COLOR_MEDIUM = '#555555';
const WALL_COLOR_LIGHT = '#777777';
const WALL_THICKNESS = 3;

const GRID_COLOR = '#';
const GRID_WIDTH = 2;
var GRID_SPACING = 24; // Can adjust to zoom in/out

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Object definitions...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Something in the world is drawable, so everything should inherit from this.
 */
function Drawable() {

}

Drawable.prototype = {
    draw: function (ctx, offX, offY) {
        alert("Inherit the draw function...");
    },
    mousemoveHandler: function () {
        alert("Inherit the mousemove function...");
    },
    mousedownHandler: function (evt) {
        alert("Inherit the mousedown function...");
    },
    mouseupHandler: function (evt) {
        alert("Inherit the mouseup function...")
    }
};

/**
 * A room is a container/"layer" for walls and other household objects.
 */
function Room() {
    this.walls = [];
}
Room.prototype = Drawable;
Room.prototype.constructor = Room;

/**
 * A wall has a start and endpoint, and fill in-between.
 */
function Wall(colA, rowA, colB, rowB) {
    this.endpointA = {c: colA, r: rowA, moving: false};
    this.endpointB = {c: colB, r: rowB, moving: false};
    this.draw = function (ctx, offX, offY) {
        // Upper-left corners of each endpoint.
        var aX = this.endpointA.c * GRID_SPACING - offX;
        var aY = this.endpointA.r * GRID_SPACING - offY;
        var bX = this.endpointB.c * GRID_SPACING - offX;
        var bY = this.endpointB.r * GRID_SPACING - offY;

        ctx.lineWidth = WALL_THICKNESS;

        // Draw a wall connecting the two endpoints. We want this wall to be of bounding thickness, so let's think about
        // this. The innermost and outermost pairs of verts won't have a line connecting them - we just want lines
        // between the middle-distance pairs of verts, to form the wall.
        ctx.fillStyle = WALL_COLOR_LIGHT;
        ctx.strokeStyle = WALL_COLOR_MEDIUM;

        // Find the quadrant that one endpoint is in relative to the other - this way we know which 2 verts need to be
        // connected with straight lines.
        if (aY > bY) {
            quadrant = aX > bX ? 1 : 2;
        } else {
            quadrant = aX > bX ? 4 : 3;
        }

        // Calculate middle endpoint positions.
        var mediumPoint1A = {x: 0, y: 0};
        var mediumPoint1B = {x: 0, y: 0};
        var mediumPoint2A = {x: 0, y: 0};
        var mediumPoint2B = {x: 0, y: 0};

        switch (quadrant) {
            case 1:
            case 3:
                mediumPoint1A.x = aX + GRID_SPACING;
                mediumPoint1A.y = aY;
                mediumPoint1B.x = bX + GRID_SPACING;
                mediumPoint1B.y = bY;

                mediumPoint2A.x = aX;
                mediumPoint2A.y = aY + GRID_SPACING;
                mediumPoint2B.x = bX;
                mediumPoint2B.y = bY + GRID_SPACING;
                break;
            case 2:
            case 4:
                mediumPoint1A.x = aX;
                mediumPoint1A.y = aY;
                mediumPoint1B.x = bX;
                mediumPoint1B.y = bY;

                mediumPoint2A.x = aX + GRID_SPACING;
                mediumPoint2A.y = aY + GRID_SPACING;
                mediumPoint2B.x = bX + GRID_SPACING;
                mediumPoint2B.y = bY + GRID_SPACING;
                break;
            default:

        }

        // Create the path between the medium points.
        ctx.beginPath();

        // Move to the first medium point, draw to its pair, draw to the second's pair, draw to the second..
        ctx.moveTo(mediumPoint1A.x, mediumPoint1A.y);
        ctx.lineTo(mediumPoint1B.x, mediumPoint1B.y);
        ctx.lineTo(mediumPoint2B.x, mediumPoint2B.y);
        ctx.lineTo(mediumPoint2A.x, mediumPoint2A.y);

        ctx.fill();
        ctx.stroke();

        // Draw handles on each endpoint.
        //TODO: Feedback fill if being dragged
        ctx.fillStyle = WALL_COLOR_LIGHT;
        ctx.strokeStyle = WALL_COLOR_DARK;
        ctx.strokeRect(aX, aY, GRID_SPACING, GRID_SPACING);
        ctx.strokeRect(bX, bY, GRID_SPACING, GRID_SPACING);

    };

    this.mousemoveHandler = function () {
        // Turn cursor if we're over a handle.
        if (mouseCol == this.endpointA.c && mouseRow == this.endpointA.r) {
            setCursorGrab();
        } else if (mouseCol == this.endpointB.c && mouseRow == this.endpointB.r) {
            setCursorGrab();
        }

        // If we're moving, keep the endpoint locations updated.
        if (this.endpointA.moving) {
            this.endpointA.r = mouseRow;
            this.endpointA.c = mouseCol;
        }
        if (this.endpointB.moving) {
            this.endpointB.r = mouseRow;
            this.endpointB.c = mouseCol;
        }
    };

    this.mousedownHandler = function (evt) {

        switch (evt.which) {
            case 1:
                // Check if there was a click in a handle - if so, set its moving property.
                if (mouseCol == this.endpointA.c && mouseRow == this.endpointA.r) {

                    this.endpointA.moving = true;
                } else if (mouseCol == this.endpointB.c && mouseRow == this.endpointB.r) {
                    this.endpointB.moving = true;
                }
                break;
            default:
                break;
        }
    };

    this.mouseupHandler = function (evt) {
        switch (evt.which) {
            case 1:
                // No matter where the click was, we are no longer moving.
                this.endpointA.moving = false;
                this.endpointB.moving = false;
                break;
            default:
                break;
        }
    };
}
Wall.prototype = Drawable;
Wall.prototype.constructor = Wall;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Instance variables...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Position - where is the viewport looking in absolute coordinates?
var worldX = 0;
var worldY = 0;

// Mouse position - x and y in viewport terms.
var mouseViewX = 0;
var mouseViewY = 0;

// Mouse row/col - in absolute coordinates.
var mouseCol = 0;
var mouseRow = 0;

// Mouse button states - true for down, false for up.
var mouse1 = false;
var mouse2 = false;
var mouse3 = false;

// All of the objects in the world.
var worldObjects = [];

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Canvas initialization...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var canvas = document.getElementById('floorplanCanvas');
var c = canvas.getContext('2d');

/**
 * Draw canvas elements.
 */
function redraw() {
    var width = canvas.clientWidth;
    var height = canvas.clientHeight;

    c.fillStyle = BACKGROUND_COLOR;
    c.fillRect(0, 0, width, height);

    // Draw a border around the canvas to make it look nice...
    c.strokeStyle = BORDER_COLOR;
    c.lineWidth = BORDER_WIDTH;
    c.strokeRect(0, 0, width, height);

    // Draw gridlines
    c.strokeStyle = GRID_COLOR;
    c.lineWidth = GRID_WIDTH;

    for (var i = 0; i < height; i += GRID_SPACING) {
        c.strokeRect(0, i - (worldY % GRID_SPACING), width, GRID_WIDTH);
    }

    for (var i = 0; i < width; i += GRID_SPACING) {
        c.strokeRect(i - (worldX % GRID_SPACING), 0, GRID_WIDTH, height);
    }

    // Draw all of the drawable objects.
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].draw(c, worldX, worldY);
    }
}

/**
 * Resolve the mouse position into row/col.
 */
function resolveMouse() {
    return {
        row: Math.floor((mouseViewY + worldY) / GRID_SPACING),
        col: Math.floor((mouseViewX + worldX) / GRID_SPACING)
    };
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Event listeners and initialization...
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Keep canvas coordinate system syncrhonized with CSS coordinates on window resize.
 */
(function () {
    var canvas = $('#floorplanCanvas');

    initCanvas();
    function initCanvas() {
        window.addEventListener('resize', resizeCanvasWithWindow, false);
        resizeCanvasWithWindow();
    }

    function resizeCanvasWithWindow() {
        var w = $('#floorplanCanvasContainer').width();
        var h = $('#floorplanCanvasContainer').height();
        canvas.attr("width", w);
        canvas.attr("height", h);
        redraw();
    }
})();

/**
 * Want to track where the mouse moved to update, also implements scroll-dragging.
 */
function mouseMoveListener(e) {
    var offset = $('#floorplanCanvas')[0].getBoundingClientRect();

    // Calculate current mouse position
    var newX = e.clientX - offset.left;
    var newY = e.clientY - offset.top;

    // Calculate deltas
    var dX = newX - mouseViewX;
    var dY = newY - mouseViewY;

    // Update global mouse position
    mouseViewX = newX;
    mouseViewY = newY;

    // Move viewport if scrolling
    if (mouse3) {
        worldX -= dX;
        worldY -= dY;
    }

    // Calculate and update column and row locations
    var cr = resolveMouse();
    mouseCol = cr.col;
    mouseRow = cr.row;

    // Reset the cursor before letting objects change it.
    setCursorDefault();

    // Propogate mouse movement events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mousemoveHandler();
    }

    redraw();
}

/**
 * Watch for mousedown events and set global flags.
 */
function mouseDownListener(e) {
    switch (e.which) {
        case 1:
            mouse1 = true;
            break;
        case 2:
            mouse2 = true;
            break;
        case 3:
            mouse3 = true;
            break;
    }

    // Propogate mousedown events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mousedownHandler(e);
    }
}

/**
 * Watch for mouseup events and set global flags.
 */
function mouseUpListener(e) {
    switch (e.which) {
        case 1:
            mouse1 = false;
            break;
        case 2:
            mouse2 = false;
            break;
        case 3:
            mouse3 = false;
            break;
    }

    // Propogate mouseup events to all world objects...
    for (var i = 0; i < worldObjects.length; i++) {
        worldObjects[i].mouseupHandler(e);
    }
}

/**
 * Watch for mousewheel events.
 */
function mouseWheelListener(e) {
    var distance = e.originalEvent.wheelDelta || - e.originalEvent.detail;  // Stupid firefox, not only does it use a
                                                                            // different event, its deltas are also
                                                                            // negative... Woo browser interop!

    if (distance > 0) {
        zoomIn();
    } else {
        zoomOut();
    }
}

// Register listeners
$('#floorplanCanvas').on('mousemove', mouseMoveListener);
$('#floorplanCanvas').on('mousedown', mouseDownListener);
$('#floorplanCanvas').on('mouseup', mouseUpListener);
$('#floorplanCanvas').on('mousewheel DOMMouseScroll', mouseWheelListener);
$('#floorplanCanvas').on('contextmenu', function () {
    return false; // Disable context menu on canvas for RMB dragging
});

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sets the cursor to a grabby cursor on the canvas.
 */
function setCursorGrab() {
    $('#floorplanCanvas').css('cursor', 'move');
}

/**
 * Sets the cursor to the default cursor on the canvas.
 */
function setCursorDefault() {
    $('#floorplanCanvas').css('cursor', 'default');
}

function zoomIn() {
    GRID_SPACING = Math.min(32, GRID_SPACING + 1);
    redraw();
}

function zoomOut() {
    GRID_SPACING = Math.max(10, GRID_SPACING - 1);
    redraw();
}

//
// Debug functions
//

function addWall() {

    var w = new Wall(1, 1, 5, 1);

    worldObjects.push(w);
}<|endoftext|>


<commit_before>import React from 'react';
import { getValueByPath } from '/imports/api/surveys/computations';

export default class Text extends React.Component {
  parseText(text) {
    const regex = /{{([^}]+)}}/g;
    const translations = {};
    let out = text;
    while (true) {
      const m = regex.exec(text);
      if (m === null) {
        break;
      }
      if (m.index === regex.lastIndex) {
        regex.lastIndex++;
      }
      m.forEach((match, groupIndex, data) => {
        if (groupIndex === 0) {
          translations[match] = data[1];
        }
      });
    }
    Object.keys(translations).forEach(t => {
      console.log(t, translations[t]); // split-join
    });
    return text;
  }

  render() {
    const { id, title, text } = this.props.item;
    const isDisabled = this.props.formState.variables[`${id}.disabled`];

    if (isDisabled) {
      return null;
    }

    return (

{title}

{this.parseText(text)}

    );
  }
}
<commit_msg>variable replacements in form text component WIP<commit_after>import React from 'react';
import { getValueByPath } from '/imports/api/surveys/computations';

export default class Text extends React.Component {
  parseText(text) {
    const regex = /{{([^}]+)}}/g;
    const translations = {};
    let out = text;
    while (true) {
      const m = regex.exec(text);
      if (m === null) {
        break;
      }
      if (m.index === regex.lastIndex) {
        regex.lastIndex++;
      }
      m.forEach((match, groupIndex, data) => {
        if (groupIndex === 0) {
          translations[match] = data[1];
        }
      });
    }
    Object.keys(translations).forEach(t => {
      const value = `TODO: ${translations[t]}`;
      out = out.split(t).join(value);
    });
    return out;
  }

  render() {
    const { id, title, text } = this.props.item;
    const isDisabled = this.props.formState.variables[`${id}.disabled`];

    if (isDisabled) {
      return null;
    }

    return (

{title}

{this.parseText(text)}

    );
  }
}
<|endoftext|>


<commit_before>//
// Renderer Process—This file is required by the index page.
// It touches the DOM by showing progress in challenge completion.
// It also handles the clear buttons and writing to user-data.
//

var fs = require('fs')
var path = require('path')
var ipc = require('electron').ipcRenderer
var userData = require(path.normalize(path.join(__dirname, '../../../lib/user-data.js')))

document.addEventListener('DOMContentLoaded', function (event) {
  var data = userData.getData()

  // Buttons
  var clearAllButtons = document.querySelectorAll('.js-clear-all-challenges')
  var leftOffButton = document.getElementById('left-off-from')
  // Sections
  var showFirstRun = document.getElementById('show-first-run')
  var showWipRun = document.getElementById('show-wip-run')
  var showFinishedRun = document.getElementById('show-finished-run')

  updateIndex(data.contents)

  // Listen for Clear All Button Events, trigger confirmation dialog
  for (var i = 0; i < clearAllButtons.length; i++) {
    clearAllButtons[ i ].addEventListener('click', function () {
      ipc.send('confirm-clear')
    }, false)
  }

  ipc.on('confirm-clear-response', function (event, response) {
    if (response === 1) return
    else clearAllChallenges(data)
  })

  // Go through each challenge in user data to see which are completed
  function updateIndex (data) {
    var circles = document.querySelectorAll('.progress-circle')
    var counter = 0
    var completed = 0

    for (var chal in data) {
      if (data[ chal ].completed) {
        // A challenge is completed so show the WIP run HTML
        showWipRun.style.display = 'block'
        showFirstRun.style.display = 'none'
        showFinishedRun.style.display = 'none'
        // Mark the corresponding circle as completed
        circles[ counter ].classList.add('completed')
        // Show the button to go to next challenge
        leftOffButton.href = path.join(__dirname, '..', 'challenges', data[ chal ].next_challenge + '.html')
        completed++
        counter++
      } else {
        counter++
      }
    }

    if (completed === 0) {
      // No challenges are complete, show the first run HTML
      showFirstRun.style.display = 'block'
      showWipRun.style.display = 'none'
      showFinishedRun.style.display = 'none'
    }

    if (completed === Object.keys(data).length) {
      // All of the challenges are complete! Show the finished run HTML
      showFirstRun.style.display = 'none'
      showWipRun.style.display = 'none'
      showFinishedRun.style.display = 'block'
    }
  }

  function clearAllChallenges (data) {
    for (var chal in data.contents) {
      if (data[ chal ].completed) {
        data[ chal ].completed = false
      }
    }
    fs.writeFileSync(data.path, JSON.stringify(data.contents, null, 2))
    // If they clear all challenges, go back to first run HTML
    var circles = document.querySelectorAll('.progress-circle')
    Array.prototype.forEach.call(circles, function (el) {
      el.classList.remove('completed')
    })

    showFirstRun.style.display = 'block'
    showWipRun.style.display = 'none'
    showFinishedRun.style.display = 'none'
  }
})
<commit_msg>Revert style changes Ref: 6b3f40178635cd9721d0b8f6e422b818dc211c62<commit_after>//
// Renderer Process—This file is required by the index page.
// It touches the DOM by showing progress in challenge completion.
// It also handles the clear buttons and writing to user-data.
//

var fs = require('fs')
var path = require('path')
var ipc = require('electron').ipcRenderer

var userData = require(path.normalize(path.join(__dirname, '../../../lib/user-data.js')))

document.addEventListener('DOMContentLoaded', function (event) {
  var data = userData.getData()

  // Buttons
  var clearAllButtons = document.querySelectorAll('.js-clear-all-challenges')
  var leftOffButton = document.getElementById('left-off-from')
  // Sections
  var showFirstRun = document.getElementById('show-first-run')
  var showWipRun = document.getElementById('show-wip-run')
  var showFinishedRun = document.getElementById('show-finished-run')

  updateIndex(data.contents)

  // Listen for Clear All Button Events, trigger confirmation dialog
  for (var i = 0; i < clearAllButtons.length; i++) {
    clearAllButtons[i].addEventListener('click', function () {
      ipc.send('confirm-clear')
    }, false)
  }

  ipc.on('confirm-clear-response', function (event, response) {
    if (response === 1) return
    else clearAllChallenges(data)
  })

  // Go through each challenge in user data to see which are completed
  function updateIndex (data) {
    var circles = document.querySelectorAll('.progress-circle')
    var counter = 0
    var completed = 0

    for (var chal in data) {
      if (data[chal].completed) {
        // A challenge is completed so show the WIP run HTML
        showWipRun.style.display = 'block'
        showFirstRun.style.display = 'none'
        showFinishedRun.style.display = 'none'
        // Mark the corresponding circle as completed
        circles[counter].classList.add('completed')
        // Show the button to go to next challenge
        leftOffButton.href = path.join(__dirname, '..', 'challenges', data[chal].next_challenge + '.html')
        completed++
        counter++
      } else {
        counter++
      }
    }

    if (completed === 0) {
      // No challenges are complete, show the first run HTML
      showFirstRun.style.display = 'block'
      showWipRun.style.display = 'none'
      showFinishedRun.style.display = 'none'
    }

    if (completed === Object.keys(data).length) {
      // All of the challenges are complete! Show the finished run HTML
      showFirstRun.style.display = 'none'
      showWipRun.style.display = 'none'
      showFinishedRun.style.display = 'block'
    }
  }

  function clearAllChallenges (data) {
    for (var chal in data.contents) {
      if (data.contents[chal].completed) {
        data.contents[chal].completed = false
     }
    }
    fs.writeFileSync(data.path, JSON.stringify(data.contents, null, 2))
    // If they clear all challenges, go back to first run HTML
    var circles = document.querySelectorAll('.progress-circle')
    Array.prototype.forEach.call(circles, function (el) {
      el.classList.remove('completed')
    })

    showFirstRun.style.display = 'block'
    showWipRun.style.display = 'none'
    showFinishedRun.style.display = 'none'
  }
})
<|endoftext|>


<commit_before>'use strict';

var SMALL_SCREEN = 500;

function init ($) {
  var lastOpenedDrawer = null;

  // Tooltips can remain in the way on touch screens.
  if (!isTouch()) {
    $('.tip').tipsy();
  } else {
    // Drawer info tips should be displayed on touchscreens.
    $('#drawer').find('.tip').tipsy();
  }
  $.fn.tipsy.defaults = {
    fade: true,
    gravity: 'n',
    opacity: 0.75
  };

  var querystring = queryParms();

  if (querystring.notify) {
    showNotification(querystring.notify, querystring.notifytype);
  }

  if (querystring.drawer) {
    openDrawer('#drawer');
  }

  $('#drawerToggle').click(function(event) {
    toggleDrawer('#drawer');
    event.preventDefault();
  });

  $('#notification').click(function(event) {
    closeNotification();
    event.preventDefault();
  });

  $('.navigation a').click(function navigationClick ( ) {
    closeDrawer('#drawer');
  });

  function reload() {
    //strip '#' so form submission does not fail
    var url = window.location.href;
    url = url.replace(/#$/, '');
    window.location = url;
  }

  function queryParms() {
    var params = {};
    if (location.search) {
      location.search.substr(1).split('&').forEach(function(item) {
        params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' ');
      });
    }
    return params;
  }

  function isTouch() {
    try { document.createEvent('TouchEvent'); return true; }
    catch (e) { return false; }
  }

  function closeDrawer(id, callback) {
    lastOpenedDrawer = null;
    $('html, body').css({ scrollTop: 0 });
    $(id).css({display: 'none', right: '-300px'});
    if (callback) { callback(); }
  }

  function openDrawer(id, prepare) {
    function closeOpenDraw(callback) {
      if (lastOpenedDrawer) {
        closeDrawer(lastOpenedDrawer, callback);
      } else {
        callback();
      }
    }

    closeOpenDraw(function () {
      lastOpenedDrawer = id;
      if (prepare) { prepare(); }

      var top = 0;
      var style = {display:'block', right: '0'};

      var windowWidth = $(window).width();
      if (windowWidth < SMALL_SCREEN || ($(window).height() < SMALL_SCREEN) && windowWidth < 800) {
        style.width = windowWidth + 'px';
        top = $("#chartContainer").offset().top - 45;
      } else {
        style.width = '350px';
      }

      style.top = top;
      style.height = ($(window).height() - top) + 'px';

      $(id).css(style);
    });

  }

  function toggleDrawer(id, openPrepare, closeCallback) {
    if (lastOpenedDrawer === id) {
      closeDrawer(id, closeCallback);
    } else {
      openDrawer(id, openPrepare);
    }
  }

  function closeNotification() {
    var notify = $('#notification');
    notify.hide();
    notify.find('span').html('');
  }

  function showNotification(note, type)  {
    var notify = $('#notification');
    notify.hide();

    // Notification types: 'info', 'warn', 'success', 'urgent'.
    // - default: 'urgent'
    notify.removeClass('info warn urgent');
    notify.addClass(type ? type : 'urgent');

    notify.find('span').html(note);
    var windowWidth = $(window).width();
    var left = (windowWidth - notify.width()) / 2;
    notify.css('left', left + 'px');
    notify.show();
  }

  function getLastOpenedDrawer() {
    return lastOpenedDrawer;
  }

  return {
    reload: reload
    , queryParms: queryParms
    , closeDrawer: closeDrawer
    , toggleDrawer: toggleDrawer
    , closeNotification: closeNotification
    , showNotification: showNotification
    , getLastOpenedDrawer: getLastOpenedDrawer
  };
}

module.exports = init;
<commit_msg>more drawer sizing fixes<commit_after>'use strict';

var SMALL_SCREEN = 500;

function init ($) {
  var lastOpenedDrawer = null;

  // Tooltips can remain in the way on touch screens.
  if (!isTouch()) {
    $('.tip').tipsy();
  } else {
    // Drawer info tips should be displayed on touchscreens.
    $('#drawer').find('.tip').tipsy();
  }
  $.fn.tipsy.defaults = {
    fade: true,
    gravity: 'n',
    opacity: 0.75
  };

  var querystring = queryParms();

  if (querystring.notify) {
    showNotification(querystring.notify, querystring.notifytype);
  }

  if (querystring.drawer) {
    openDrawer('#drawer');
  }

  $('#drawerToggle').click(function(event) {
    toggleDrawer('#drawer');
    event.preventDefault();
  });

  $('#notification').click(function(event) {
    closeNotification();
    event.preventDefault();
  });

  $('.navigation a').click(function navigationClick ( ) {
    closeDrawer('#drawer');
  });

  function reload() {
    //strip '#' so form submission does not fail
    var url = window.location.href;
    url = url.replace(/#$/, '');
    window.location = url;
  }

  function queryParms() {
    var params = {};
    if (location.search) {
      location.search.substr(1).split('&').forEach(function(item) {
        params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' ');
      });
    }
    return params;
  }

  function isTouch() {
    try { document.createEvent('TouchEvent'); return true; }
    catch (e) { return false; }
  }

  function closeDrawer(id, callback) {
    lastOpenedDrawer = null;
    $('html, body').css({ scrollTop: 0 });
    $(id).css({display: 'none', right: '-300px'});
    if (callback) { callback(); }
  }

  function openDrawer(id, prepare) {
    function closeOpenDraw(callback) {
      if (lastOpenedDrawer) {
        closeDrawer(lastOpenedDrawer, callback);
      } else {
        callback();
      }
    }

    closeOpenDraw(function () {
      lastOpenedDrawer = id;
      if (prepare) { prepare(); }

      var style = {display:'block', right: '0'};

      var windowWidth = $(window).width();
      if (windowWidth < SMALL_SCREEN || ($(window).height() < SMALL_SCREEN) && windowWidth < 800) {
        style.width = windowWidth + 'px';
        var top = $("#chartContainer").offset().top - 45;
        style.top = top + 'px';
        style.height = ($(window).height() - top - 45) + 'px';
      } else {
        style.top = '0px';
        style.height = ($(window).height() - 45) + 'px';
        style.width = '350px';
      }

      $(id).css(style);
    });

  }

  function toggleDrawer(id, openPrepare, closeCallback) {
    if (lastOpenedDrawer === id) {
      closeDrawer(id, closeCallback);
    } else {
      openDrawer(id, openPrepare);
    }
  }

  function closeNotification() {
    var notify = $('#notification');
    notify.hide();
    notify.find('span').html('');
  }

  function showNotification(note, type)  {
    var notify = $('#notification');
    notify.hide();

    // Notification types: 'info', 'warn', 'success', 'urgent'.
    // - default: 'urgent'
    notify.removeClass('info warn urgent');
    notify.addClass(type ? type : 'urgent');

    notify.find('span').html(note);
    var windowWidth = $(window).width();
    var left = (windowWidth - notify.width()) / 2;
    notify.css('left', left + 'px');
    notify.show();
  }

  function getLastOpenedDrawer() {
    return lastOpenedDrawer;
  }

  return {
    reload: reload
    , queryParms: queryParms
    , closeDrawer: closeDrawer
    , toggleDrawer: toggleDrawer
    , closeNotification: closeNotification
    , showNotification: showNotification
    , getLastOpenedDrawer: getLastOpenedDrawer
  };
}

module.exports = init;
<|endoftext|>


<commit_before>const _ = require('lodash'),
	utils = require('../util/utils'),
	taskManager = require('../common/manager/taskManager'),
	pomelo = require('../pomelo'),
	rsa = require('node-bignumber'),
	events = require('../util/events'),
	logger = require('pomelo-logger').getLogger('pomelo', __filename);

class Connector
{
	/**
	 * Connector component. Receive client requests and attach session with socket.
	 *
	 * @param {Object} app  current application context
	 * @param {Object} opts attach parameters
	 *                      opts.connector {Object} provides low level network and protocol details implementation between server and clients.
	 */
	constructor(app, opts)
	{
		opts = opts || {};
		this.app = app;
		this.connector = ConnectorUtility.GetConnector(app, opts);
		this.encode = opts.encode;
		this.decode = opts.decode;
		this.useCrypto = opts.useCrypto;
		this.blacklistFun = opts.blacklistFun;
		this.keys = {};
		this.blacklist = [];

		if (opts.useDict)
		{
			app.load(pomelo.dictionary, app.get('dictionaryConfig'));
		}

		if (opts.useProtobuf)
		{
			app.load(pomelo.protobuf, app.get('protobufConfig'));
		}

		// component dependencies
		this.server = null;
		this.session = null;
		this.connection = null;
		this.name = '__connector__';
	}

	start(cb)
	{
		const components = this.app.components;

		this.server = _.get(components, '__server__', null);
		this.session = _.get(components, '__session__', null);
		this.connection = _.get(components, '__connection__', null);

		// check component dependencies
		if (!this.server)
		{
			process.nextTick(() =>
			{
				utils.invokeCallback(cb, new Error('fail to start connector component for no server component loaded'));
			});
			return;
		}

		if (!this.session)
		{
			process.nextTick(() =>
			{
				utils.invokeCallback(cb, new Error('fail to start connector component for no session component loaded'));
			});
			return;
		}

		process.nextTick(cb);
	}

	afterStart(cb)
	{
		this.connector.start(cb);
		this.connector.on('connection', ConnectorUtility.HostFilter.bind(this, ConnectorUtility.BindEvents));
	}

	stop(force, cb)
	{
		if (this.connector)
		{
			this.connector.stop(force, cb);
			this.connector = null;
		}
		else
		{
			process.nextTick(cb);
		}
	}

	send(reqId, route, msg, recvs, opts, cb)
	{
		logger.debug(`[${this.app.serverId}] send message reqId: ${reqId}, route: ${route}, msg: ${msg}, receivers: ${recvs}, opts: ${opts}`);
		let emsg = msg;
		if (this.encode)
		{
			// use costumized encode
			emsg = this.encode(reqId, route, msg);
		}
		else if (this.connector.encode)
		{
			// use connector default encode
			emsg = this.connector.encode(reqId, route, msg);
		}

		if (!emsg)
		{
			process.nextTick(function()
			{
				utils.invokeCallback(cb, new Error('fail to send message for encode result is empty.'));
				return;
			});
		}

		this.app.components.__pushScheduler__.schedule(reqId, route, emsg, recvs, opts, cb);
	}

	setPubKey(id, key)
	{
		const pubKey = new rsa.Key();
		pubKey.n = new rsa.BigInteger(key.rsa_n, 16);
		pubKey.e = key.rsa_e;
		this.keys[id] = pubKey;
	}

	getPubKey(id)
	{
		return this.keys[id];
	}
}

class ConnectorUtility
{
	static GetConnector(app, opts)
	{
		const connector = opts.connector;
		if (!connector)
		{
			return ConnectorUtility.GetDefaultConnector(app, opts);
		}

		if (!_.isFunction(connector))
		{
			return connector;
		}

		const curServer = app.getCurServer();
		return connector(curServer.clientPort, curServer.host, opts);
	}

	static GetDefaultConnector(app, opts)
	{
		const DefaultConnector = require('../connectors/sioConnector');
		const curServer = app.getCurServer();
		return new DefaultConnector(curServer.clientPort, curServer.host, opts);
	}

	static HostFilter(cb, socket)
	{

		// dynamical check
		if (this.blacklist.length !== 0 && ConnectorUtility.HostCheck(socket, this.blacklist))
		{
			return;
		}
		// static check
		if (this.blacklistFun && _.isFunction(this.blacklistFun))
		{
			this.blacklistFun((err, list) =>
			{
				if (err)
				{
					logger.error(`connector blacklist error: ${err.stack}`);
					utils.invokeCallback(cb, this, socket);
					return;
				}
				if (!Array.isArray(list))
				{
					logger.error('connector blacklist is not array: %j', list);
					utils.invokeCallback(cb, this, socket);
					return;
				}
				if (!ConnectorUtility.HostCheck(socket, list))
				{
					utils.invokeCallback(cb, this, socket);
				}
			});
		}
		else
		{
			utils.invokeCallback(cb, this, socket);
		}
	}

	static HostCheck(socket, list)
	{
		const ip = socket.remoteAddress.ip;
		for (const address in list)
		{
			const exp = new RegExp(list[address]);
			if (exp.test(ip))
			{
				socket.disconnect();
				return true;
			}
		}
		return false;
	}

	static BindEvents(connector, socket)
	{
		if (connector.connection)
		{
			connector.connection.increaseConnectionCount();
			const statisticInfo = connector.connection.getStatisticsInfo();
			const curServer = connector.app.getCurServer();
			if (statisticInfo.totalConnCount > curServer['max-connections'])
			{
				logger.warn(`the server ${curServer.id} has reached the max connections ${curServer['max-connections']}`);
				socket.disconnect();
				return;
			}
		}

		// create session for connection
		const session = ConnectorUtility.GetSession(connector, socket);
		let closed = false;

		socket.on('disconnect', () =>
		{
			if (closed)
			{
				return;
			}
			closed = true;
			if (connector.connection)
			{
				connector.connection.decreaseConnectionCount(session.uid);
			}
		});

		socket.on('error', function()
		{
			if (closed)
			{
				return;
			}
			closed = true;
			if (connector.connection)
			{
				connector.connection.decreaseConnectionCount(session.uid);
			}
		});

		// new message
		socket.on('message', msg =>
		{
			let dmsg = msg;
			if (connector.decode)
			{
				dmsg = connector.decode(msg, session);
			}
			else if (connector.connector.decode)
			{
				dmsg = connector.connector.decode(msg);
			}
			if (!dmsg)
			{
				// discard invalid message
				return;
			}

			// use rsa crypto
			if (connector.useCrypto)
			{
				const verified = ConnectorUtility.VerifyMessage(connector, session, dmsg);
				if (!verified)
				{
					logger.error('fail to verify the data received from client.');
					return;
				}
			}

			ConnectorUtility.HandleMessage(connector, session, dmsg);
		}); // on message end
	}

	/**
	 * get session for current connection
	 */
	static GetSession(connector, socket)
	{
		const app = connector.app, sid = socket.id;
		let session = connector.session.get(sid);
		if (session)
		{
			return session;
		}

		session = connector.session.create(sid, app.getServerId(), socket);
		logger.debug(`[${app.getServerId()}] getSession session is created with session id: ${sid}`);

		// bind events for session
		socket.on('disconnect', session.closed.bind(session));
		socket.on('error', session.closed.bind(session));
		session.on('closed', ConnectorUtility.OnSessionClose.bind(null, app));
		session.on('bind', uid =>
		{
			logger.debug('session on [%s] bind with uid: %s', connector.app.serverId, uid);
			// update connection statistics if necessary
			if (connector.connection)
			{
				connector.connection.addLoginedUser(uid, {
					loginTime : Date.now(),
					uid       : uid,
					address   : `${socket.remoteAddress.ip}:${socket.remoteAddress.port}`
				});
			}
			connector.app.event.emit(events.BIND_SESSION, session);
		});

		session.on('unbind', function(uid)
		{
			if (connector.connection)
			{
				connector.connection.removeLoginedUser(uid);
			}
			connector.app.event.emit(events.UNBIND_SESSION, session);
		});

		return session;
	}

	static OnSessionClose(app, session, reason)
	{
		taskManager.closeQueue(session.id, true);
		app.event.emit(events.CLOSE_SESSION, session);
	}

	static HandleMessage(connector, session, msg)
	{
		logger.debug(`[${connector.app.serverId}] handleMessage session id: ${session.id}, msg: ${msg}`);
		const type = ConnectorUtility.CheckServerType(msg.route);
		if (!type)
		{
			logger.error(`invalid route string. route : ${msg.route}`);
			return;
		}
		connector.server.globalHandle(msg, session.toFrontendSession(), function(err, resp, opts)
		{
			if (resp && !msg.id)
			{
				logger.warn(`try to response to a notify : ${msg.route}`);
				return;
			}
			if (!msg.id && !resp) return;
			if (!resp) resp = {};
			if (err && !resp.code)
			{
				resp.code = 500;
			}
			opts = {
				type        : 'response',
				userOptions : opts || {}
			};
			// for compatiablity
			opts.isResponse = true;

			connector.send(msg.id, msg.route, resp, [session.id], opts, () =>
			{
				// null
			});
		});
	}

	/**
	 * Get server type form request message.
	 */
	static CheckServerType(route)
	{
		if (!route)
		{
			return null;
		}
		const idx = route.indexOf('.');
		if (idx < 0)
		{
			return null;
		}
		return route.substring(0, idx);
	}

	static VerifyMessage(connector, session, msg)
	{
		const sig = msg.body.__crypto__;
		if (!sig)
		{
			logger.error('receive data from client has no signature [%s]', connector.app.serverId);
			return false;
		}

		let pubKey;

		if (!session)
		{
			logger.error('could not find session.');
			return false;
		}

		if (!session.get('pubKey'))
		{
			pubKey = connector.getPubKey(session.id);
			if (pubKey)
			{
				delete connector.keys[session.id];
				session.set('pubKey', pubKey);
			}
			else
			{
				logger.error('could not get public key, session id is %s', session.id);
				return false;
			}
		}
		else
		{
			pubKey = session.get('pubKey');
		}

		if (!pubKey.n || !pubKey.e)
		{
			logger.error('could not verify message without public key [%s]', connector.app.serverId);
			return false;
		}

		delete msg.body.__crypto__;

		let message = JSON.stringify(msg.body);
		if (utils.hasChineseChar(message))
			message = utils.unicodeToUtf8(message);

		return pubKey.verifyString(message, sig);
	}

}

module.exports = function(app, opts)
{
	return new Connector(app, opts);
};<commit_msg>bug: 修复连接数达到max-connections后，connection计数多1的bug<commit_after>const _ = require('lodash'),
	utils = require('../util/utils'),
	taskManager = require('../common/manager/taskManager'),
	pomelo = require('../pomelo'),
	rsa = require('node-bignumber'),
	events = require('../util/events'),
	logger = require('pomelo-logger').getLogger('pomelo', __filename);

class Connector
{
	/**
	 * Connector component. Receive client requests and attach session with socket.
	 *
	 * @param {Object} app  current application context
	 * @param {Object} opts attach parameters
	 *                      opts.connector {Object} provides low level network and protocol details implementation between server and clients.
	 */
	constructor(app, opts)
	{
		opts = opts || {};
		this.app = app;
		this.connector = ConnectorUtility.GetConnector(app, opts);
		this.encode = opts.encode;
		this.decode = opts.decode;
		this.useCrypto = opts.useCrypto;
		this.blacklistFun = opts.blacklistFun;
		this.keys = {};
		this.blacklist = [];

		if (opts.useDict)
		{
			app.load(pomelo.dictionary, app.get('dictionaryConfig'));
		}

		if (opts.useProtobuf)
		{
			app.load(pomelo.protobuf, app.get('protobufConfig'));
		}

		// component dependencies
		this.server = null;
		this.session = null;
		this.connection = null;
		this.name = '__connector__';
	}

	start(cb)
	{
		const components = this.app.components;

		this.server = _.get(components, '__server__', null);
		this.session = _.get(components, '__session__', null);
		this.connection = _.get(components, '__connection__', null);

		// check component dependencies
		if (!this.server)
		{
			process.nextTick(() =>
			{
				utils.invokeCallback(cb, new Error('fail to start connector component for no server component loaded'));
			});
			return;
		}

		if (!this.session)
		{
			process.nextTick(() =>
			{
				utils.invokeCallback(cb, new Error('fail to start connector component for no session component loaded'));
			});
			return;
		}

		process.nextTick(cb);
	}

	afterStart(cb)
	{
		this.connector.start(cb);
		this.connector.on('connection', ConnectorUtility.HostFilter.bind(this, ConnectorUtility.BindEvents));
	}

	stop(force, cb)
	{
		if (this.connector)
		{
			this.connector.stop(force, cb);
			this.connector = null;
		}
		else
		{
			process.nextTick(cb);
		}
	}

	send(reqId, route, msg, recvs, opts, cb)
	{
		logger.debug(`[${this.app.serverId}] send message reqId: ${reqId}, route: ${route}, msg: ${msg}, receivers: ${recvs}, opts: ${opts}`);
		let emsg = msg;
		if (this.encode)
		{
			// use costumized encode
			emsg = this.encode(reqId, route, msg);
		}
		else if (this.connector.encode)
		{
			// use connector default encode
			emsg = this.connector.encode(reqId, route, msg);
		}

		if (!emsg)
		{
			process.nextTick(function()
			{
				utils.invokeCallback(cb, new Error('fail to send message for encode result is empty.'));
				return;
			});
		}

		this.app.components.__pushScheduler__.schedule(reqId, route, emsg, recvs, opts, cb);
	}

	setPubKey(id, key)
	{
		const pubKey = new rsa.Key();
		pubKey.n = new rsa.BigInteger(key.rsa_n, 16);
		pubKey.e = key.rsa_e;
		this.keys[id] = pubKey;
	}

	getPubKey(id)
	{
		return this.keys[id];
	}
}

class ConnectorUtility
{
	static GetConnector(app, opts)
	{
		const connector = opts.connector;
		if (!connector)
		{
			return ConnectorUtility.GetDefaultConnector(app, opts);
		}

		if (!_.isFunction(connector))
		{
			return connector;
		}

		const curServer = app.getCurServer();
		return connector(curServer.clientPort, curServer.host, opts);
	}

	static GetDefaultConnector(app, opts)
	{
		const DefaultConnector = require('../connectors/sioConnector');
		const curServer = app.getCurServer();
		return new DefaultConnector(curServer.clientPort, curServer.host, opts);
	}

	static HostFilter(cb, socket)
	{

		// dynamical check
		if (this.blacklist.length !== 0 && ConnectorUtility.HostCheck(socket, this.blacklist))
		{
			return;
		}
		// static check
		if (this.blacklistFun && _.isFunction(this.blacklistFun))
		{
			this.blacklistFun((err, list) =>
			{
				if (err)
				{
					logger.error(`connector blacklist error: ${err.stack}`);
					utils.invokeCallback(cb, this, socket);
					return;
				}
				if (!Array.isArray(list))
				{
					logger.error('connector blacklist is not array: %j', list);
					utils.invokeCallback(cb, this, socket);
					return;
				}
				if (!ConnectorUtility.HostCheck(socket, list))
				{
					utils.invokeCallback(cb, this, socket);
				}
			});
		}
		else
		{
			utils.invokeCallback(cb, this, socket);
		}
	}

	static HostCheck(socket, list)
	{
		const ip = socket.remoteAddress.ip;
		for (const address in list)
		{
			const exp = new RegExp(list[address]);
			if (exp.test(ip))
			{
				socket.disconnect();
				return true;
			}
		}
		return false;
	}

	static BindEvents(connector, socket)
	{
		if (connector.connection)
		{
			const statisticInfo = connector.connection.getStatisticsInfo();
			const curServer = connector.app.getCurServer();
			if (statisticInfo.totalConnCount > curServer['max-connections'])
			{
				logger.warn(`the server ${curServer.id} has reached the max connections ${curServer['max-connections']}`);
				socket.disconnect();
				return;
			}
			connector.connection.increaseConnectionCount();
		}

		// create session for connection
		const session = ConnectorUtility.GetSession(connector, socket);
		let closed = false;

		socket.on('disconnect', () =>
		{
			if (closed)
			{
				return;
			}
			closed = true;
			if (connector.connection)
			{
				connector.connection.decreaseConnectionCount(session.uid);
			}
		});

		socket.on('error', function()
		{
			if (closed)
			{
				return;
			}
			closed = true;
			if (connector.connection)
			{
				connector.connection.decreaseConnectionCount(session.uid);
			}
		});

		// new message
		socket.on('message', msg =>
		{
			let dmsg = msg;
			if (connector.decode)
			{
				dmsg = connector.decode(msg, session);
			}
			else if (connector.connector.decode)
			{
				dmsg = connector.connector.decode(msg);
			}
			if (!dmsg)
			{
				// discard invalid message
				return;
			}

			// use rsa crypto
			if (connector.useCrypto)
			{
				const verified = ConnectorUtility.VerifyMessage(connector, session, dmsg);
				if (!verified)
				{
					logger.error('fail to verify the data received from client.');
					return;
				}
			}

			ConnectorUtility.HandleMessage(connector, session, dmsg);
		}); // on message end
	}

	/**
	 * get session for current connection
	 */
	static GetSession(connector, socket)
	{
		const app = connector.app, sid = socket.id;
		let session = connector.session.get(sid);
		if (session)
		{
			return session;
		}

		session = connector.session.create(sid, app.getServerId(), socket);
		logger.debug(`[${app.getServerId()}] getSession session is created with session id: ${sid}`);

		// bind events for session
		socket.on('disconnect', session.closed.bind(session));
		socket.on('error', session.closed.bind(session));
		session.on('closed', ConnectorUtility.OnSessionClose.bind(null, app));
		session.on('bind', uid =>
		{
			logger.debug('session on [%s] bind with uid: %s', connector.app.serverId, uid);
			// update connection statistics if necessary
			if (connector.connection)
			{
				connector.connection.addLoginedUser(uid, {
					loginTime : Date.now(),
					uid       : uid,
					address   : `${socket.remoteAddress.ip}:${socket.remoteAddress.port}`
				});
			}
			connector.app.event.emit(events.BIND_SESSION, session);
		});

		session.on('unbind', function(uid)
		{
			if (connector.connection)
			{
				connector.connection.removeLoginedUser(uid);
			}
			connector.app.event.emit(events.UNBIND_SESSION, session);
		});

		return session;
	}

	static OnSessionClose(app, session, reason)
	{
		taskManager.closeQueue(session.id, true);
		app.event.emit(events.CLOSE_SESSION, session);
	}

	static HandleMessage(connector, session, msg)
	{
		logger.debug(`[${connector.app.serverId}] handleMessage session id: ${session.id}, msg: ${msg}`);
		const type = ConnectorUtility.CheckServerType(msg.route);
		if (!type)
		{
			logger.error(`invalid route string. route : ${msg.route}`);
			return;
		}
		connector.server.globalHandle(msg, session.toFrontendSession(), function(err, resp, opts)
		{
			if (resp && !msg.id)
			{
				logger.warn(`try to response to a notify : ${msg.route}`);
				return;
			}
			if (!msg.id && !resp) return;
			if (!resp) resp = {};
			if (err && !resp.code)
			{
				resp.code = 500;
			}
			opts = {
				type        : 'response',
				userOptions : opts || {}
			};
			// for compatiablity
			opts.isResponse = true;

			connector.send(msg.id, msg.route, resp, [session.id], opts, () =>
			{
				// null
			});
		});
	}

	/**
	 * Get server type form request message.
	 */
	static CheckServerType(route)
	{
		if (!route)
		{
			return null;
		}
		const idx = route.indexOf('.');
		if (idx < 0)
		{
			return null;
		}
		return route.substring(0, idx);
	}

	static VerifyMessage(connector, session, msg)
	{
		const sig = msg.body.__crypto__;
		if (!sig)
		{
			logger.error('receive data from client has no signature [%s]', connector.app.serverId);
			return false;
		}

		let pubKey;

		if (!session)
		{
			logger.error('could not find session.');
			return false;
		}

		if (!session.get('pubKey'))
		{
			pubKey = connector.getPubKey(session.id);
			if (pubKey)
			{
				delete connector.keys[session.id];
				session.set('pubKey', pubKey);
			}
			else
			{
				logger.error('could not get public key, session id is %s', session.id);
				return false;
			}
		}
		else
		{
			pubKey = session.get('pubKey');
		}

		if (!pubKey.n || !pubKey.e)
		{
			logger.error('could not verify message without public key [%s]', connector.app.serverId);
			return false;
		}

		delete msg.body.__crypto__;

		let message = JSON.stringify(msg.body);
		if (utils.hasChineseChar(message))
			message = utils.unicodeToUtf8(message);

		return pubKey.verifyString(message, sig);
	}

}

module.exports = function(app, opts)
{
	return new Connector(app, opts);
};<|endoftext|>