<commit_before>import classNames from 'classnames';
import {Form} from 'reactjs-components';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
import React from 'react';

import QueryParamsMixin from '../mixins/QueryParamsMixin';

class SidebarFilter extends mixin(QueryParamsMixin) {
  constructor() {
    super();

    this.state = {
      selectedNodes: []
    };
  }

  componentDidMount() {
    this.updateFilterStatus();
  }

  componentWillReceiveProps() {
    this.updateFilterStatus();
  }

  handleFormChange(model, eventObj) {
    let filterValue = this.props.filterValues[eventObj.fieldValue.name];

    if (eventObj.fieldValue.checked) {
      this.setFilterNode(filterValue);
    } else {
      this.unsetFilterNode(filterValue);
    }
  }

  setFilterNode(filterValue) {
    let selectedNodes = this.state.selectedNodes.slice(0);

    selectedNodes.push(filterValue);

    this.setQueryParam(this.props.filterType, selectedNodes);
  }

  unsetFilterNode(filterValue) {
    let {state} = this;
    let selectedNodes = [];

    let index = state.selectedNodes
      .indexOf(filterValue.toString());

    if (index !== -1) {
      selectedNodes = state.selectedNodes.slice(0);
      selectedNodes.splice(index, 1);
    }

    this.setQueryParam(this.props.filterType, selectedNodes);
  }

  updateFilterStatus() {
    let {props, state} = this;
    let {filterType, filterValues} = props;
    let selectedNodes = this.getQueryParamObject()[filterType] || [];
    let stringify = JSON.stringify;

    if (!Array.isArray(selectedNodes)) {
      selectedNodes = decodeURIComponent(selectedNodes)
        .split(',')
        .filter(function (filterValue) {
          let existingNode =
            Object.values(filterValues).indexOf(parseInt(filterValue, 10));
          return existingNode !== -1;
        });
    }

    if (stringify(selectedNodes) !== stringify(state.selectedNodes)) {
      this.setState({selectedNodes},
        this.props.handleFilterChange.bind(null, selectedNodes, filterType));
    }
  }

  getCountByValue(filterValue) {
    let {props} = this;
    let count = props.countByValue[props.filterValues[filterValue]];

    if (count == null) {
      return 0;
    }

    return count;
  }

  getClearLinkForFilter(filterQueryParamKey) {
    let {router} = this.context;
    let currentPathname = router.getCurrentPathname();
    let query = Object.assign({}, router.getCurrentQuery());
    let params = router.getCurrentParams();

    if (query[filterQueryParamKey] == null ||
      query[filterQueryParamKey].length === 0) {
      return null;
    }

    if (query[filterQueryParamKey] != null) {
      delete query[filterQueryParamKey];
    }

    return (
      <Link
        className="small"
        to={currentPathname}
        query={query}
        params={params}>
        (Clear)
      </Link>
    );
  }

  getFormLabel(filterLabel, filterValue) {
    let badge = null;
    let count = this.getCountByValue(filterValue);

    if (count) {
      badge = (

          {count}

      );
    }

    return (

        {filterLabel}
        {badge}

    );
  }

  getHealthCheckboxes() {
    let {filterLabels, filterValues} = this.props;
    let {selectedNodes} = this.state;

    return Object.keys(filterValues).map(filterValue => {
      let value = filterValues[filterValue].toString();
      let checked = selectedNodes.indexOf(value) > -1;

      let labelClassSet = classNames(
        'side-list-item form-row-element form-element-checkbox ' +
        'inverse row row-flex flush clickable',
        {
          'filter-active': this.getCountByValue(filterValue) > 0,
          'filter-checked': checked
        }
      );

      return {
        checked,
        checkboxLabelClass: 'form-element-checkbox-label flex-grow',
        value: checked,
        fieldType: 'checkbox',
        formElementClass: 'form-row-element checkbox flush',
        name: filterValue,
        label:
          this.getFormLabel(filterLabels[filterValue], filterValue),
        labelClass: labelClassSet
      };
    });
  }

  getHealthNodes() {
    let definition = [{
      fieldType: 'checkboxMultiple',
      name: 'healthNodes',
      value: this.getHealthCheckboxes(),
      writeType: 'input'
    }];

    return (
      <Form
        formGroupClass="form-group flush"
        formRowClass="row"
        definition={definition}
        onChange={this.handleFormChange.bind(this)} />
    );
  }

  getTitle() {
    let {title} = this.props;

    if (title == null) {
      return null;
    }

    return (

{title}

    );
  }

  render() {
    let {props} = this;

    return (

          {this.getTitle()}
          {this.getClearLinkForFilter(props.filterType)}

        {this.getHealthNodes()}

    );
  }
}

SidebarFilter.propTypes = {
  countByValue: React.PropTypes.object.isRequired,
  filterLabels: React.PropTypes.object.isRequired,
  filterType: React.PropTypes.string.isRequired,
  filterValues: React.PropTypes.object.isRequired,
  handleFilterChange: React.PropTypes.func.isRequired,
  title: React.PropTypes.string
};

module.exports = SidebarFilter;
<commit_msg>Using checkbox as multiple<commit_after>import classNames from 'classnames';
import {Form} from 'reactjs-components';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
import React from 'react';

import QueryParamsMixin from '../mixins/QueryParamsMixin';

class SidebarFilter extends mixin(QueryParamsMixin) {
  constructor() {
    super();

    this.state = {
      selectedNodes: []
    };
  }

  componentDidMount() {
    this.updateFilterStatus();
  }

  componentWillReceiveProps() {
    this.updateFilterStatus();
  }

  handleFormChange(model, eventObj) {
    let filterValue = this.props.filterValues[eventObj.fieldValue.name];

    if (eventObj.fieldValue.checked) {
      this.setFilterNode(filterValue);
    } else {
      this.unsetFilterNode(filterValue);
    }
  }

  setFilterNode(filterValue) {
    let selectedNodes = this.state.selectedNodes.slice(0);

    selectedNodes.push(filterValue);

    this.setQueryParam(this.props.filterType, selectedNodes);
  }

  unsetFilterNode(filterValue) {
    let {state} = this;
    let selectedNodes = [];

    let index = state.selectedNodes
      .indexOf(filterValue.toString());

    if (index !== -1) {
      selectedNodes = state.selectedNodes.slice(0);
      selectedNodes.splice(index, 1);
    }

    this.setQueryParam(this.props.filterType, selectedNodes);
  }

  updateFilterStatus() {
    let {props, state} = this;
    let {filterType, filterValues} = props;
    let selectedNodes = this.getQueryParamObject()[filterType] || [];
    let stringify = JSON.stringify;

    if (!Array.isArray(selectedNodes)) {
      selectedNodes = decodeURIComponent(selectedNodes)
        .split(',')
        .filter(function (filterValue) {
          let existingNode =
            Object.values(filterValues).indexOf(parseInt(filterValue, 10));
          return existingNode !== -1;
        });
    }

    if (stringify(selectedNodes) !== stringify(state.selectedNodes)) {
      this.setState({selectedNodes},
        this.props.handleFilterChange.bind(null, selectedNodes, filterType));
    }
  }

  getCountByValue(filterValue) {
    let {props} = this;
    let count = props.countByValue[props.filterValues[filterValue]];

    if (count == null) {
      return 0;
    }

    return count;
  }

  getClearLinkForFilter(filterQueryParamKey) {
    let {router} = this.context;
    let currentPathname = router.getCurrentPathname();
    let query = Object.assign({}, router.getCurrentQuery());
    let params = router.getCurrentParams();

    if (query[filterQueryParamKey] == null ||
      query[filterQueryParamKey].length === 0) {
      return null;
    }

    if (query[filterQueryParamKey] != null) {
      delete query[filterQueryParamKey];
    }

    return (
      <Link
        className="small"
        to={currentPathname}
        query={query}
        params={params}>
        (Clear)
      </Link>
    );
  }

  getFormLabel(filterLabel, filterValue) {
    let badge = null;
    let count = this.getCountByValue(filterValue);

    if (count) {
      badge = (

          {count}

      );
    }

    return (

        {filterLabel}
        {badge}

    );
  }

  getHealthCheckboxes() {
    let {filterLabels, filterValues} = this.props;
    let {selectedNodes} = this.state;

    return Object.keys(filterValues).map(filterValue => {
      let value = filterValues[filterValue].toString();
      let checked = selectedNodes.indexOf(value) > -1;

      let labelClassSet = classNames(
        'side-list-item form-row-element form-element-checkbox ' +
        'inverse row row-flex flush clickable',
        {
          'filter-active': this.getCountByValue(filterValue) > 0,
          'filter-checked': checked
        }
      );

      return {
        checked,
        checkboxLabelClass: 'form-element-checkbox-label flex-grow',
        value: checked,
        fieldType: 'checkbox',
        formElementClass: 'form-row-element checkbox flush',
        name: filterValue,
        label:
          this.getFormLabel(filterLabels[filterValue], filterValue),
        labelClass: labelClassSet
      };
    });
  }

  getHealthNodes() {
    let definition = [{
      fieldType: 'checkbox',
      name: 'healthNodes',
      value: this.getHealthCheckboxes(),
      writeType: 'input'
    }];

    return (
      <Form
        formGroupClass="form-group flush"
        formRowClass="row"
        definition={definition}
        onChange={this.handleFormChange.bind(this)} />
    );
  }

  getTitle() {
    let {title} = this.props;

    if (title == null) {
      return null;
    }

    return (

{title}

    );
  }

  render() {
    let {props} = this;

    return (

          {this.getTitle()}
          {this.getClearLinkForFilter(props.filterType)}

        {this.getHealthNodes()}

    );
  }
}

SidebarFilter.propTypes = {
  countByValue: React.PropTypes.object.isRequired,
  filterLabels: React.PropTypes.object.isRequired,
  filterType: React.PropTypes.string.isRequired,
  filterValues: React.PropTypes.object.isRequired,
  handleFilterChange: React.PropTypes.func.isRequired,
  title: React.PropTypes.string
};

module.exports = SidebarFilter;
<|endoftext|>


<commit_before>/*global jQuery */
/*eslint no-unused-vars: 0*/
var ChromeBootstrap = {
  init: function() {
    // Menu functionality for chrome-bootstrap
    jQuery('.menu').on("click", "a", function(ev) {
      jQuery('.mainview > *').removeClass("selected");
      jQuery('.menu li').removeClass("selected");
      jQuery('.mainview > *:not(.selected)').css('display', 'none');

      jQuery(ev.currentTarget).parent().addClass("selected");
      var currentView = jQuery(jQuery(ev.currentTarget).attr('href'));
      currentView.css('display', 'block');
      currentView.addClass("selected");
      jQuery('body')[0].scrollTop = 0;
    });

    jQuery('.mainview > *:not(.selected)').css('display', 'none');

    // Activate navigationitem via hashtag
    jQuery(window).on("load", function () {
      var hash = window.location.hash;
      if (hash) {
        var main = hash.replace(/-.*$/, "");
        var sub = hash.replace(/^.*?-/, "");
        var $nav = jQuery(".navigation a[href='" + main + "']");
        if ($nav.length === 1) {
          $nav.trigger("click");
        }

        // Activate sub-item
        if (sub !== "") {
          window.location.hash = "#help";
          window.location.hash = hash;
        }
      }
    });

  }
};
<commit_msg>allow linking of help pages from options<commit_after>/*global jQuery */
/*eslint no-unused-vars: 0*/
var ChromeBootstrap = {
  init: function() {
    // Menu functionality for chrome-bootstrap
    jQuery('.menu').on("click", "a", function(ev) {
      jQuery('.mainview > *').removeClass("selected");
      jQuery('.menu li').removeClass("selected");
      jQuery('.mainview > *:not(.selected)').css('display', 'none');

      jQuery(ev.currentTarget).parent().addClass("selected");
      var currentView = jQuery(jQuery(ev.currentTarget).attr('href'));
      currentView.css('display', 'block');
      currentView.addClass("selected");
      jQuery('body')[0].scrollTop = 0;
    });

    jQuery('.mainview > *:not(.selected)').css('display', 'none');

    var relocate = function(target) {
      var hash = window.location.hash;

      if (typeof target !== "undefined") {
        hash = target;
      }

      if (hash) {
        var main = hash.replace(/-.*$/, "");
        var sub = hash.replace(/^.*?-/, "");
        var $nav = jQuery(".navigation a[href='" + main + "']");
        if ($nav.length === 1) {
          $nav.trigger("click");
        }

        // Activate sub-item
        if (sub !== "") {
          window.location.hash = "#help";
          window.location.hash = hash;
        }
      }
    };

    // Activate navigationitem via hashtag
    jQuery(window).on("load", relocate);

    jQuery(document).on("click", "a[href*=#help-]", function() {
      relocate(this.href.replace(/^.*#/,"#"));
    });
  }
};
<|endoftext|>


<commit_before>import classNames from 'classnames';
import {Confirm} from 'reactjs-components';
import mixin from 'reactjs-mixin';
import prettycron from 'prettycron';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {routerShape} from 'react-router';

import {StoreMixin} from 'mesosphere-shared-reactjs';

import Icon from '../../components/Icon';
import JobConfiguration from './JobConfiguration';
import JobFormModal from '../../components/modals/JobFormModal';
import JobRunHistoryTable from './JobRunHistoryTable';
import JobsBreadcrumbs from '../../components/breadcrumbs/JobsBreadcrumbs';
import Loader from '../../components/Loader';
import MetronomeStore from '../../stores/MetronomeStore';
import Page from '../../components/Page';
import RequestErrorMsg from '../../components/RequestErrorMsg';
import StringUtil from '../../utils/StringUtil';
import TabsMixin from '../../mixins/TabsMixin';
import TimeAgo from '../../components/TimeAgo';
import TaskStates from '../../../../plugins/services/src/js/constants/TaskStates';

const METHODS_TO_BIND = [
  'closeDialog',
  'handleEditButtonClick',
  'handleRunNowButtonClick',
  'handleDisableScheduleButtonClick',
  'handleEnableScheduleButtonClick',
  'handleDestroyButtonClick',
  'onMetronomeStoreJobDeleteError',
  'onMetronomeStoreJobDeleteSuccess',
  'onMetronomeStoreJobDetailError',
  'onMetronomeStoreJobDetailChange'
];

const JobActionItem = {
  EDIT: 'edit',
  DESTROY: 'destroy'
};

class JobDetailPage extends mixin(StoreMixin, TabsMixin) {
  constructor() {
    super(...arguments);

    this.store_listeners = [{
      name: 'metronome',
      events: [
        'jobDeleteSuccess',
        'jobDeleteError',
        'jobDetailChange',
        'jobDetailError',
        'jobRunError',
        'jobRunSuccess',
        'jobScheduleUpdateError',
        'jobScheduleUpdateSuccess'
      ],
      suppressUpdate: false
    }];

    this.tabs_tabs = {
      runHistory: 'Run History',
      configuration: 'Configuration'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      disabledDialog: null,
      jobActionDialog: null,
      errorMsg: null,
      errorCount: 0,
      isJobFormModalOpen: false,
      isLoading: true
    };

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);
    MetronomeStore.monitorJobDetail(this.props.params.id);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);
    MetronomeStore.stopJobDetailMonitor(this.props.params.id);
  }

  onMetronomeStoreJobDeleteError(id, {message:errorMsg}) {
    if (id !== this.props.params.id || errorMsg == null) {
      return;
    }

    this.setState({
      jobActionDialog: JobActionItem.DESTROY,
      disabledDialog: null,
      errorMsg
    });
  }

  onMetronomeStoreJobDeleteSuccess() {
    this.closeDialog();
    this.context.router.push('/jobs');
  }

  onMetronomeStoreJobDetailError() {
    this.setState({errorCount: this.state.errorCount + 1});
  }

  onMetronomeStoreJobDetailChange() {
    this.setState({errorCount: 0, isLoading: false});
  }

  handleEditButtonClick() {
    this.setState({jobActionDialog: JobActionItem.EDIT});
  }

  handleRunNowButtonClick() {
    let job = MetronomeStore.getJob(this.props.params.id);

    MetronomeStore.runJob(job.getId());
  }

  handleDisableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, false);
  };

  handleEnableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, true);
  };

  handleDestroyButtonClick() {
    this.setState({jobActionDialog: JobActionItem.DESTROY});
  };

  handleAcceptDestroyDialog(stopCurrentJobRuns = false) {
    this.setState({disabledDialog: JobActionItem.DESTROY}, () => {
      MetronomeStore.deleteJob(this.props.params.id, stopCurrentJobRuns);
    });
  }

  handleMoreDropdownSelection(selection) {
    if (selection.id === JobActionItem.SCHEDULE_DISABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, false);
      return;
    }

    if (selection.id === JobActionItem.SCHEDULE_ENABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, true);
      return;
    }

    this.setState({jobActionDialog: selection.id});
  }

  closeDialog() {
    this.setState({
      disabledDialog: null,
      errorMsg: null,
      jobActionDialog: null
    });
  }

  getDestroyConfirmDialog() {
    const {id} = this.props.params;
    const {disabledDialog, jobActionDialog, errorMsg} = this.state;
    let stopCurrentJobRuns = false;
    let actionButtonLabel = 'Destroy Job';
    let message = `Are you sure you want to destroy ${id}? ` +
      'This action is irreversible.';

    if (/stopCurrentJobRuns=true/.test(errorMsg)) {
      actionButtonLabel = 'Stop Current Runs and Destroy Job';
      stopCurrentJobRuns = true;
      message = `Couldn't destroy ${id} as there are currently active job ` +
        'runs. Do you want to stop all runs and destroy the job?';
    }

    let content = (

          Destroy Job

{message}

    );

    return (
      <Confirm children={content}
        disabled={disabledDialog === JobActionItem.DESTROY}
        open={jobActionDialog === JobActionItem.DESTROY}
        onClose={this.closeDialog}
        leftButtonText="Cancel"
        leftButtonCallback={this.closeDialog}
        rightButtonText={actionButtonLabel}
        rightButtonClassName="button button-danger"
        rightButtonCallback=
          {this.handleAcceptDestroyDialog.bind(this, stopCurrentJobRuns)} />
    );
  }

  getErrorScreen() {
    return (

        <RequestErrorMsg />

    );
  }

  getLoadingScreen() {
    return (
      <Page>
        <Page.Header breadcrumbs={<JobsBreadcrumbs/>} />
        <Loader />
      </Page>
    );
  }

  getNavigationTabs() {
    return (

        {this.tabs_getUnroutedTabs()}

    );
  }

  getPrettySchedule(job) {
    let schedules = job.getSchedules();
    if (schedules == null || schedules.length === 0) {
      return null;
    }

    let schedule = schedules[0];
    if (schedule.enabled) {
      return prettycron.toString(schedule.cron);
    }
  }

  getSubTitle(job) {
    let nodes = [];
    let scheduleText = this.getPrettySchedule(job);
    let longestRunningTask = null;
    let longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null && scheduleText == null) {
      return null;
    }

    if (scheduleText != null) {
      nodes.push(

          <Icon
            className="icon-margin-right"
            key="schedule-icon"
            color="grey"
            id="repeat"
            size="mini" />
          Scheduled {StringUtil.lowercase(scheduleText)}

      );
    }

    if (longestRunningTask != null) {
      let dateCompleted = longestRunningTask.getDateCompleted();

      let status = TaskStates[longestRunningTask.getStatus()];
      let statusClasses = classNames('job-details-header-status', {
        'text-success': status.stateTypes.includes('success')
        && !status.stateTypes.includes('failure'),
        'text-danger': status.stateTypes.includes('failure')
      });

      let timePrefix = null;
      let shouldSuppressRelativeTime = dateCompleted == null;

      if (shouldSuppressRelativeTime) {
        timePrefix = 'for ';
      }

      nodes.push(

          {status.displayName}
        ,

          <TimeAgo
            prefix={timePrefix}
            suppressSuffix={shouldSuppressRelativeTime}
            time={longestRunningActiveRun.getDateCreated()} />

      );
    }

    return nodes;
  }

  renderConfigurationTabView(job) {
    return <JobConfiguration job={job} />;
  }

  renderRunHistoryTabView(job) {
    return <JobRunHistoryTable job={job} />;
  }

  getActions() {
    let job = MetronomeStore.getJob(this.props.params.id);
    let [schedule] = job.getSchedules();

    const actions = [];

    actions.push({
      label: 'Edit',
      onItemSelect: this.handleEditButtonClick
    });

    actions.push({
      label: 'Run Now',
      onItemSelect: this.handleRunNowButtonClick
    });

    if (schedule != null && schedule.enabled) {
      actions.push({
        label: 'Disable Schedule',
        onItemSelect: this.handleDisableScheduleButtonClick
      });
    }

    if (schedule != null && !schedule.enabled) {
      actions.push({
        label: 'Enable Schedule',
        onItemSelect: this.handleEnableScheduleButtonClick
      });
    }

    actions.push({
      className: 'text-danger',
      label: 'Destroy',
      onItemSelect: this.handleDestroyButtonClick
    });

    return actions;
  }

  getTabs() {
    return [
      {
        label: 'Run History', callback: () => {
          this.setState({currentTab: 'runHistory'});
        }
      },
      {
        label: 'Configuration', callback: () => {
          this.setState({currentTab: 'configuration'});
        }
      }
    ];
  }

  render() {
    if (this.state.errorCount > 3) {
      return this.getErrorScreen();
    }

    if (this.state.isLoading) {
      return this.getLoadingScreen();
    }

    // TaskDetailView
    if (this.props.params.taskID) {
      return this.props.children;
    }

    let job = MetronomeStore.getJob(this.props.params.id);

    return (
        <Page>
          <Page.Header
              actions={this.getActions()}
              breadcrumbs={<JobsBreadcrumbs jobID={job.getId()} />}
              tabs={this.getTabs()} />
          {this.tabs_getTabView(job)}
          <JobFormModal isEdit={true}
              job={job}
              open={this.state.jobActionDialog === JobActionItem.EDIT}
              onClose={this.closeDialog} />
          {this.getDestroyConfirmDialog()}
        </Page>
    );
  }
}

JobDetailPage.contextTypes = {
  router: routerShape
};

module.exports = JobDetailPage;
<commit_msg>Adjust dialog const name<commit_after>import classNames from 'classnames';
import {Confirm} from 'reactjs-components';
import mixin from 'reactjs-mixin';
import prettycron from 'prettycron';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {routerShape} from 'react-router';

import {StoreMixin} from 'mesosphere-shared-reactjs';

import Icon from '../../components/Icon';
import JobConfiguration from './JobConfiguration';
import JobFormModal from '../../components/modals/JobFormModal';
import JobRunHistoryTable from './JobRunHistoryTable';
import JobsBreadcrumbs from '../../components/breadcrumbs/JobsBreadcrumbs';
import Loader from '../../components/Loader';
import MetronomeStore from '../../stores/MetronomeStore';
import Page from '../../components/Page';
import RequestErrorMsg from '../../components/RequestErrorMsg';
import StringUtil from '../../utils/StringUtil';
import TabsMixin from '../../mixins/TabsMixin';
import TimeAgo from '../../components/TimeAgo';
import TaskStates from '../../../../plugins/services/src/js/constants/TaskStates';

const METHODS_TO_BIND = [
  'closeDialog',
  'handleEditButtonClick',
  'handleRunNowButtonClick',
  'handleDisableScheduleButtonClick',
  'handleEnableScheduleButtonClick',
  'handleDestroyButtonClick',
  'onMetronomeStoreJobDeleteError',
  'onMetronomeStoreJobDeleteSuccess',
  'onMetronomeStoreJobDetailError',
  'onMetronomeStoreJobDetailChange'
];

const DIALOGS = {
  EDIT: 'edit',
  DESTROY: 'destroy'
};

class JobDetailPage extends mixin(StoreMixin, TabsMixin) {
  constructor() {
    super(...arguments);

    this.store_listeners = [{
      name: 'metronome',
      events: [
        'jobDeleteSuccess',
        'jobDeleteError',
        'jobDetailChange',
        'jobDetailError',
        'jobRunError',
        'jobRunSuccess',
        'jobScheduleUpdateError',
        'jobScheduleUpdateSuccess'
      ],
      suppressUpdate: false
    }];

    this.tabs_tabs = {
      runHistory: 'Run History',
      configuration: 'Configuration'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      disabledDialog: null,
      jobActionDialog: null,
      errorMsg: null,
      errorCount: 0,
      isJobFormModalOpen: false,
      isLoading: true
    };

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);
    MetronomeStore.monitorJobDetail(this.props.params.id);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);
    MetronomeStore.stopJobDetailMonitor(this.props.params.id);
  }

  onMetronomeStoreJobDeleteError(id, {message:errorMsg}) {
    if (id !== this.props.params.id || errorMsg == null) {
      return;
    }

    this.setState({
      jobActionDialog: DIALOGS.DESTROY,
      disabledDialog: null,
      errorMsg
    });
  }

  onMetronomeStoreJobDeleteSuccess() {
    this.closeDialog();
    this.context.router.push('/jobs');
  }

  onMetronomeStoreJobDetailError() {
    this.setState({errorCount: this.state.errorCount + 1});
  }

  onMetronomeStoreJobDetailChange() {
    this.setState({errorCount: 0, isLoading: false});
  }

  handleEditButtonClick() {
    this.setState({jobActionDialog: DIALOGS.EDIT});
  }

  handleRunNowButtonClick() {
    let job = MetronomeStore.getJob(this.props.params.id);

    MetronomeStore.runJob(job.getId());
  }

  handleDisableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, false);
  };

  handleEnableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, true);
  };

  handleDestroyButtonClick() {
    this.setState({jobActionDialog: DIALOGS.DESTROY});
  };

  handleAcceptDestroyDialog(stopCurrentJobRuns = false) {
    this.setState({disabledDialog: DIALOGS.DESTROY}, () => {
      MetronomeStore.deleteJob(this.props.params.id, stopCurrentJobRuns);
    });
  }

  handleMoreDropdownSelection(selection) {
    if (selection.id === DIALOGS.SCHEDULE_DISABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, false);
      return;
    }

    if (selection.id === DIALOGS.SCHEDULE_ENABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, true);
      return;
    }

    this.setState({jobActionDialog: selection.id});
  }

  closeDialog() {
    this.setState({
      disabledDialog: null,
      errorMsg: null,
      jobActionDialog: null
    });
  }

  getDestroyConfirmDialog() {
    const {id} = this.props.params;
    const {disabledDialog, jobActionDialog, errorMsg} = this.state;
    let stopCurrentJobRuns = false;
    let actionButtonLabel = 'Destroy Job';
    let message = `Are you sure you want to destroy ${id}? ` +
      'This action is irreversible.';

    if (/stopCurrentJobRuns=true/.test(errorMsg)) {
      actionButtonLabel = 'Stop Current Runs and Destroy Job';
      stopCurrentJobRuns = true;
      message = `Couldn't destroy ${id} as there are currently active job ` +
        'runs. Do you want to stop all runs and destroy the job?';
    }

    let content = (

          Destroy Job

{message}

    );

    return (
      <Confirm children={content}
        disabled={disabledDialog === DIALOGS.DESTROY}
        open={jobActionDialog === DIALOGS.DESTROY}
        onClose={this.closeDialog}
        leftButtonText="Cancel"
        leftButtonCallback={this.closeDialog}
        rightButtonText={actionButtonLabel}
        rightButtonClassName="button button-danger"
        rightButtonCallback=
          {this.handleAcceptDestroyDialog.bind(this, stopCurrentJobRuns)} />
    );
  }

  getErrorScreen() {
    return (

        <RequestErrorMsg />

    );
  }

  getLoadingScreen() {
    return (
      <Page>
        <Page.Header breadcrumbs={<JobsBreadcrumbs/>} />
        <Loader />
      </Page>
    );
  }

  getNavigationTabs() {
    return (

        {this.tabs_getUnroutedTabs()}

    );
  }

  getPrettySchedule(job) {
    let schedules = job.getSchedules();
    if (schedules == null || schedules.length === 0) {
      return null;
    }

    let schedule = schedules[0];
    if (schedule.enabled) {
      return prettycron.toString(schedule.cron);
    }
  }

  getSubTitle(job) {
    let nodes = [];
    let scheduleText = this.getPrettySchedule(job);
    let longestRunningTask = null;
    let longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null && scheduleText == null) {
      return null;
    }

    if (scheduleText != null) {
      nodes.push(

          <Icon
            className="icon-margin-right"
            key="schedule-icon"
            color="grey"
            id="repeat"
            size="mini" />
          Scheduled {StringUtil.lowercase(scheduleText)}

      );
    }

    if (longestRunningTask != null) {
      let dateCompleted = longestRunningTask.getDateCompleted();

      let status = TaskStates[longestRunningTask.getStatus()];
      let statusClasses = classNames('job-details-header-status', {
        'text-success': status.stateTypes.includes('success')
        && !status.stateTypes.includes('failure'),
        'text-danger': status.stateTypes.includes('failure')
      });

      let timePrefix = null;
      let shouldSuppressRelativeTime = dateCompleted == null;

      if (shouldSuppressRelativeTime) {
        timePrefix = 'for ';
      }

      nodes.push(

          {status.displayName}
        ,

          <TimeAgo
            prefix={timePrefix}
            suppressSuffix={shouldSuppressRelativeTime}
            time={longestRunningActiveRun.getDateCreated()} />

      );
    }

    return nodes;
  }

  renderConfigurationTabView(job) {
    return <JobConfiguration job={job} />;
  }

  renderRunHistoryTabView(job) {
    return <JobRunHistoryTable job={job} />;
  }

  getActions() {
    let job = MetronomeStore.getJob(this.props.params.id);
    let [schedule] = job.getSchedules();

    const actions = [];

    actions.push({
      label: 'Edit',
      onItemSelect: this.handleEditButtonClick
    });

    actions.push({
      label: 'Run Now',
      onItemSelect: this.handleRunNowButtonClick
    });

    if (schedule != null && schedule.enabled) {
      actions.push({
        label: 'Disable Schedule',
        onItemSelect: this.handleDisableScheduleButtonClick
      });
    }

    if (schedule != null && !schedule.enabled) {
      actions.push({
        label: 'Enable Schedule',
        onItemSelect: this.handleEnableScheduleButtonClick
      });
    }

    actions.push({
      className: 'text-danger',
      label: 'Destroy',
      onItemSelect: this.handleDestroyButtonClick
    });

    return actions;
  }

  getTabs() {
    return [
      {
        label: 'Run History', callback: () => {
          this.setState({currentTab: 'runHistory'});
        }
      },
      {
        label: 'Configuration', callback: () => {
          this.setState({currentTab: 'configuration'});
        }
      }
    ];
  }

  render() {
    if (this.state.errorCount > 3) {
      return this.getErrorScreen();
    }

    if (this.state.isLoading) {
      return this.getLoadingScreen();
    }

    // TaskDetailView
    if (this.props.params.taskID) {
      return this.props.children;
    }

    let job = MetronomeStore.getJob(this.props.params.id);

    return (
        <Page>
          <Page.Header
              actions={this.getActions()}
              breadcrumbs={<JobsBreadcrumbs jobID={job.getId()} />}
              tabs={this.getTabs()} />
          {this.tabs_getTabView(job)}
          <JobFormModal isEdit={true}
              job={job}
              open={this.state.jobActionDialog === DIALOGS.EDIT}
              onClose={this.closeDialog} />
          {this.getDestroyConfirmDialog()}
        </Page>
    );
  }
}

JobDetailPage.contextTypes = {
  router: routerShape
};

module.exports = JobDetailPage;
<|endoftext|>


<commit_before>import React from 'react';
import * as keyService from "./baseKey"
import BLOCKCHAIN_INFO from "../../../../env"
import WalletLink from "walletlink"
import Web3 from "web3"

export default class WalletLinkKey extends React.Component {

    constructor(props) {
        super(props);

        const APP_NAME = "My Awesome App"
        const APP_LOGO_URL = "https://example.com/logo.png"

        this.walletLink = new WalletLink({
            appName: APP_NAME,
            appLogoUrl: APP_LOGO_URL
        })

        this.ethereum = this.walletLink.makeWeb3Provider(BLOCKCHAIN_INFO["connections"]["http"][1]["endPoint"], BLOCKCHAIN_INFO.networkId)

        this.web3 = new Web3(this.ethereum)

    }

    getAddress = () => {
        return new Promise((resolve, reject) => {
            this.ethereum.enable().then((accounts) => {
                resolve(accounts[0])
            }).catch(err => {
                console.log(err)
                reject(err.message)
            })
        })
    }

    async broadCastTx(funcName, ...args) {
        try {
            var txHash = await this.callSignTransaction(funcName, ...args)
            return txHash
        } catch (err) {
            console.log(err)
            throw err
        }
    }

    callSignTransaction = (funcName, ...args) => {
        return new Promise((resolve, reject) => {
            keyService[funcName](...args).then(result => {
                const { txParams, keystring, password } = result
                this.sealTx(txParams, keystring, password).then(result => {
                    resolve(result)
                }).catch(e => {
                    console.log(e.message)
                    reject(e)
                })
            })
        })
    }

    sealTx = (txParams, keystring, password) => {
        txParams.gas = txParams.gasLimit
        delete (txParams.gasLimit)

        return new Promise((resolve, reject) => {
            this.web3.eth.sendTransaction(txParams, function (err, transactionHash) {
                if (!err) {
                    resolve(transactionHash)
                } else {
                    console.log(err)
                    reject(err.message)
                }
            })
        })
    }

    async signSignature(message, account) {
        try {
            // console.log(account.address)
            var signature = await this.web3.eth.sign(message, account.address)
            console.log(signature)
            return signature
        } catch (err) {
            console.log(err)
            throw err
        }
    }

    getWalletName = () => {
        return 'Wallet Link';
    }
}
<commit_msg>fix walletlink signature<commit_after>import React from 'react';
import * as keyService from "./baseKey"
import BLOCKCHAIN_INFO from "../../../../env"
import WalletLink from "walletlink"
import Web3 from "web3"

export default class WalletLinkKey extends React.Component {

    constructor(props) {
        super(props);

        const APP_NAME = "My Awesome App"
        const APP_LOGO_URL = "https://example.com/logo.png"

        this.walletLink = new WalletLink({
            appName: APP_NAME,
            appLogoUrl: APP_LOGO_URL
        })

        this.ethereum = this.walletLink.makeWeb3Provider(BLOCKCHAIN_INFO["connections"]["http"][1]["endPoint"], BLOCKCHAIN_INFO.networkId)

        this.web3 = new Web3(this.ethereum)

    }

    getAddress = () => {
        return new Promise((resolve, reject) => {
            this.ethereum.enable().then((accounts) => {
                resolve(accounts[0])
            }).catch(err => {
                console.log(err)
                reject(err.message)
            })
        })
    }

    async broadCastTx(funcName, ...args) {
        try {
            var txHash = await this.callSignTransaction(funcName, ...args)
            return txHash
        } catch (err) {
            console.log(err)
            throw err
        }
    }

    callSignTransaction = (funcName, ...args) => {
        return new Promise((resolve, reject) => {
            keyService[funcName](...args).then(result => {
                const { txParams, keystring, password } = result
                this.sealTx(txParams, keystring, password).then(result => {
                    resolve(result)
                }).catch(e => {
                    console.log(e.message)
                    reject(e)
                })
            })
        })
    }

    sealTx = (txParams, keystring, password) => {
        txParams.gas = txParams.gasLimit
        delete (txParams.gasLimit)

        return new Promise((resolve, reject) => {
            this.web3.eth.sendTransaction(txParams, function (err, transactionHash) {
                if (!err) {
                    resolve(transactionHash)
                } else {
                    console.log(err)
                    reject(err.message)
                }
            })
        })
    }

    async signSignature(message, account) {
        try {
            console.log(this.web3)
            var signature = await this.web3.eth.personal.sign(message, account.address)
            console.log(signature)
            return signature
        } catch (err) {
            console.log(err)
            throw err
        }
    }

    getWalletName = () => {
        return 'Wallet Link';
    }
}
<|endoftext|>


<commit_before>var WorkerWrapperSandbox = {},
    noop = null;

noop = function() {
    return function () {};
};

WorkerWrapperSandbox.postMessage = noop();

WorkerWrapperSandbox.close = noop();

WorkerWrapperSandbox.onmessage = null;

WorkerWrapperSandbox.addEventListener = function (event, listener) {
    this.onmessage = listener;
    return;
};

WorkerWrapperSandbox.loadWorker = function () {
    var self = this;

    (function () {
        /**
         * @module WebWorker
         */
        var Action = null,
            Event = null,
            _listeners = null;

        Action = {
            "START": 'start',
            "SET_TERMINATING_STATUS": '_setTerminatingStatus',
            "TERMINATE": 'terminate',
            "TERMINATE_NOW": 'terminateNow',
            "TRIGGER": 'trigger',
            "TRIGGER_SELF": 'triggerSelf'
        };

        Event = {
            "INITIALIZED": 'initialized',
            "ERROR": 'error',

            "WORKER_LOADING": 'worker-loading',
            "WORKER_LOADED": 'worker-loaded',

            "WORKER_STARTING": 'worker-starting',
            "WORKER_STARTED": 'worker-started',

            "WORKER_TERMINATING": 'worker-terminating',
            "WORKER_TERMINATED": 'worker-terminated'
        };

        // Add eventPrefix to all event types
        for (var key in Event) {
            Event[key] = 'webworker:' + Event[key];
        }

        /**
         * This documentation is for the worker script _wrapper_. Every user webworker
         * script is wrapped within this shell. All properties and methods described
         * here are attached to the `'self'` object within the worker script.
         * @class Wrapper
         * @static
         */

        /**
         * Boolean indicating whether the worker (wrapper) script has initialized.
         * @property _isInitialized
         * @type {Boolean}
         * @private
         * @default false
         */
        self._isInitialized = false;

        /**
         * Object in which all internal worker listeners are tracked.
         * @property _listeners
         * @type {Object}
         * @private
         */
        _listeners = {};
        self._listeners = _listeners;

        /**
         * Boolean indicating if the worker script has been initiated to be terminated.
         * @property _isTerminating
         * @type {Boolean}
         * @private
         * @default false
         */
        self._isTerminating = false;

        /**
         * List of pre-defined actions for the worker. This value is inherited from the
         * base class. Refer to {{#crossLink "WebWorker/Action:property"}}{{/crossLink}}
         * for more information.
         * @property Action
         * @type {Object}
         */
        self.Action = Action;

        /**
         * List of pre-defined event types for the worker. This value is inherited from the
         * base class. Refer to {{#crossLink "WebWorker/Event:property"}}{{/crossLink}}
         * for more information.
         * @property Event
         * @type {Object}
         */
        self.Event = Event;

        /**
         * This method is called by the script whenever the worker is made to terminate.
         * By default this method does nothing but it exists so that you can override it
         * with a custom function to possibly perform cleanup operations if the worker is
         * being made to terminate.
         * @method terminateHandler
         */
        self.terminateHandler = null;

        /**
         * Returns boolean indicating whether the worker (wrapper) script has initialized.
         * @method isInitialized
         * @return {Boolean} Boolean indicating whether the worker (wrapper) script has initialized.
         */
        self.isInitialized = function () {
            return self._isInitialized;
        };

        /**
         * Returns boolean indicating if the worker script has been initiated to be terminated.
         * @method isTerminating
         * @return {Boolean} Boolean indicating if the worker script has been initiated to be terminated.
         */
        self.isTerminating = function () {
            return self._isTerminating;
        };

        /**
         * The main worker script that is provided by the user is injected into this function.
         * Also any arguments passed to the {{#crossLink "Wrapper/start:method"}}{{/crossLink}} method are passed on to this function.
         * @method _main
         * @private
         * @chainable
         */
        self._main = function () {
            return self;
        };

        /**
         * This method is called when initializing the worker script.
         * It also triggers the WORKER_LOADED event on the base worker instance.
         * @method init
         * @chainable
         */
        self.init = function () {
            self._isInitialized = true;

            self.trigger(Event.WORKER_LOADED);

            return self;
        };

        /**
         * This method is called by the base worker instance when it's {{#crossLink "WebWorker/start:method"}}{{/crossLink}}
         * method is invoked. It also gets the arguments from the base worker.
         * Internally it calls the {{#crossLink "Wrapper/main:method"}}{{/crossLink}} method with arguments.
         * @method start
         * @chainable
         */
        self.start = function () {
            if (!self.isInitialized()) {
                return self;
            }

            self._main.apply(self, arguments);

            self.trigger(Event.WORKER_STARTED);
            return self;
        };

        /**
         * This method emulates basic jQuery `.on` event binding behavior.
         * @method on
         * @param {String} eventType The string event type that needs to be bound to.
         * @param {Function} listener The function that will listen to the event.
         * @chainable
         */
        self.on = function (eventType, listener) {
            eventType += '';
            listener = listener || null;

            if (typeof listener !== 'function') {
                return self;
            }

            if (!(eventType in _listeners)) {
                _listeners[eventType] = [];
            }

            _listeners[eventType].push(listener);
            return self;
        };

        /**
         * This method emulates basic jQuery `.one` event binding behavior.
         * @method one
         * @param {String} eventType The string event type that needs to be bound to.
         * @param {Function} listener The function that will listen to the event.
         * @chainable
         */
        self.one = function (eventType, oneListener) {
            var wrapperListener = null;

            wrapperListener = function () {
                oneListener.apply(this, arguments);
                self.off(eventType, wrapperListener);
                return;
            };

            self.on(eventType, wrapperListener);
            return;
        };

        /**
         * This method emulates basic jQuery `.off` event unbinding behavior.
         * @method off
         * @param {String} [eventType] The string event type that needs to be unbound.
         *                             If not provided or passed as `null` all event
         *                             listeners are unbound.
         * @param {Function} [listener] The function that needs to be unbound from the event.
         *                              If not provided or passed as `null` then all event
         *                              listeners for that particular event type are unbound.
         * @chainable
         */
        self.off = function (eventType, listener) {
            var key = null;

            eventType = eventType || null;
            listener = listener || null;

            if (eventType === null && listener === null) {
                for (key in _listeners) {
                    delete _listeners[key];
                }
                return self;
            }

            self._removeListenerFromEventType(eventType, listener);

            return self;
        };

        /**
         * Removes an event listener for a particular event type. Optionally if
         * the listener is not passed or is `null` it removes all listeners for
         * the specified event type.
         * @method _removeListenerFromEventType
         * @private
         * @param  {String} eventType Event type for which the listener is
         *                            being removed.
         * @param  {Function} [listener]  The listener that needs to be removed.
         *                                If not passed in or passed as `null`
         *                                all listeners for the specified event
         *                                type are removed.
         * @chainable
         */
        self._removeListenerFromEventType = function (eventType, listener) {
            var listeners = _listeners[eventType],
                i = 0;

            listener = listener || null;
            if (listener === null) {
                _listeners[eventType] = [];
                return self;
            }

            for (; i < listeners.length; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    i--;
                }
            }
            return self;
        };

        /**
         * Trigger events on the base worker instance.
         * @method trigger
         * @param  {String|Object} event The event to be triggered. Generally
         *                               expected to be a `String` but can
         *                               also be an `Object`.
         * @param  {Mixed} [data] Optional data to associate with the event.
         * @chainable
         */
        self.trigger = function (event, data) {

            var eventType = null,
                hasData = null;

            hasData = typeof data !== 'undefined';

            event = event || null;

            if (event === null) {
                return self;
            }

            if (typeof event === 'string') {
                eventType = event || null;
                if (hasData) {
                    event = {
                        "type": eventType,
                        "data": data
                    };
                }
            }
            else if (typeof event === 'object') {
                eventType = event.type || null;
                data = event.data;
            }

            if (eventType === null) {
                return self;
            }

            self.sendMessage(Action.TRIGGER, [event]);
            return self;
        };

        /**
         * Trigger events on the thread worker instance.
         * @method triggerSelf
         * @param  {String|Object} event The event to be triggered. Generally
         *                               expected to be a `String` but can
         *                               also be an `Object`.
         * @param  {Mixed} [data] Optional data to associate with the event.
         * @chainable
         */
        self.triggerSelf = function (event, data) {

            var self = this,
                eventType = null,
                listeners = null,
                listenersCount = null,
                listener = null,
                i = 0;

            event = event || null;

            if (event === null) {
                return self;
            }

            if (typeof event === 'string') {
                eventType = event || null;
            }
            else if (typeof event === 'object') {
                eventType = event.type || null;
                data = event.data;
            }

            if (eventType === null) {
                return self;
            }

            event.data = data;

            listeners = _listeners[eventType] || null;
            if (listeners === null) {
                return self;
            }

            listenersCount = listeners.length;

            for (; i < listenersCount; i++) {
                listener = listeners[i];
                listener.apply(self, [event]);
                if (listenersCount !== listeners.length) {
                    i--;
                    listenersCount = listeners.length;
                }
            }

            return self;
        };

        /**
         * This method is used to format messages being sent between the worker
         * and the base worker instance. It's usage is similar to the
         * {{#crossLink "WebWorker/sendMessage:method"}}{{/crossLink}} method
         * in the base class.
         * @method sendMessage
         * @param {String} action The name of the method on the base worker instance to call.
         * @param {Array} args An array of arguments to pass to the action.
         * @chainable
         */
        self.sendMessage = function (action, args) {
            var message = null;

            action = action || null;
            args = args || null;

            if (action === null) {
                return self;
            }

            message = {
                "__isWebWorkerMsg": true
            };

            message.action = action;
            message.args = args;

            self.postMessage(message);

            return self;
        };

        /**
         * Terminates the worker from within the worker script.
         * Note that terminates from within the worker are asynchronous.
         * This method calls the
         * {{#crossLink "Wrapper/terminateHandler"}}{{/crossLink}},
         * if it is assigned and is a function, and then goes on to
         * communicate with the base worker class to call the method
         * {{#crossLink "WebWorker/terminateNow:method"}}{{/crossLink}}.
         *
         * The {{#crossLink "WebWorker"}}{{/crossLink}}
         * class does provide a synchronous way to terminate the worker
         * but it is not supported from within the worker script itself.
         *
         * @method terminate
         * @param {Boolean} [terminateNow] If set to `true` the worker
         *                                 also does a `self.close()`
         *                                 after it communicates the
         *                                 terminate status to the base
         *                                 worker instance.
         *                                 It is recommended to use the method
         *                                 {{#crossLink "Wrapper/terminateNow:method"}}{{/crossLink}}
         *                                 to achieve the same goal as
         *                                 it gives a more clear semantics
         *                                 in your code.
         * @chainable
         */
        self.terminate = function (terminateNow) {
            var terminateHandler = null,
                returnValue = null;

            terminateNow = !!terminateNow;
            terminateHandler = self.terminateHandler || null;

            if (!self.isTerminating()) {
                self._setTerminatingStatus(true);
                self.trigger(Event.WORKER_TERMINATING);
            }

            if (typeof terminateHandler === 'function') {
                returnValue = terminateHandler.apply(self, arguments);
            }

            self.sendMessage(Action.TERMINATE_NOW, [returnValue]);

            if (terminateNow) {
                self._nativeClose();
            }

            return self;
        };

        /**
         * Terminates the worker _immediately_ from within the worker script.
         * This method internally calls the
         * {{#crossLink "Wrapper/terminate:method"}}{{/crossLink}} method
         * to terminate the worker.
         *
         * @method terminateNow
         */
        self.terminateNow = function () {
            self.terminate(true);
            return;
        };

        /**
         * Set the terminating status. Used internally by the wrapper script
         * to keep track of the terminating status.
         * @method _setTerminatingStatus
         * @private
         * @param {Boolean} status The status you want to set.
         * @chainable
         */
        self._setTerminatingStatus = function (status) {
            self._isTerminating = status;
            return self;
        };

        /**
         * The native worker `close` method is overwritten in the wrapper to
         * give consistent behaviour with the WebWorker class.
         * This method serves as an alias for the native version of the
         * method to be used internally.
         * @method _nativeClose
         * @private
         */
        self._nativeClose = self.close;

        /**
         * The native `close` method is overwritten to serve as an alias for
         * {{#crossLink "Wrapper/terminate:method"}}{{/crossLink}}. This
         * has been done to give a consistent behaviour for the script.
         * You are encouraged to use
         * {{#crossLink "Wrapper/terminateNow:method"}}{{/crossLink}}
         * to terminate the script immediately.
         * @method close
         * @chainable
         */
        self.close = self.terminate;

        // Add a handler for the message event
        self.addEventListener('message', function (event) {
            var originalEvent = event.originalEvent || event,
                msg = originalEvent.data,
                action = null,
                args = null;

            if (typeof msg === 'object'
                && '__isWebWorkerMsg' in msg
                && msg.__isWebWorkerMsg) {
                action = msg.action;
                args = msg.args;

                self[action].apply(self, args);
                return;
            }

            return;
        }, false);

        // Initialize the worker
        self.init();

        return;
    })();
    return;
};
<commit_msg>Fix some bugs, change visibility of init method and update documentation.<commit_after>var WorkerWrapperSandbox = {},
    noop = null;

noop = function() {
    return function () {};
};

WorkerWrapperSandbox.postMessage = noop();

WorkerWrapperSandbox.close = noop();

WorkerWrapperSandbox.onmessage = null;

WorkerWrapperSandbox.addEventListener = function (event, listener) {
    this.onmessage = listener;
    return;
};

WorkerWrapperSandbox.loadWorker = function () {
    var self = this;

    (function () {
        /**
         * @module WebWorker
         */
        var Action = null,
            Event = null,
            _listeners = null;

        Action = {
            "START": 'start',
            "SET_TERMINATING_STATUS": '_setTerminatingStatus',
            "TERMINATE": 'terminate',
            "TERMINATE_NOW": 'terminateNow',
            "TRIGGER": 'trigger',
            "TRIGGER_SELF": 'triggerSelf'
        };

        Event = {
            "INITIALIZED": 'initialized',
            "ERROR": 'error',

            "WORKER_LOADING": 'worker-loading',
            "WORKER_LOADED": 'worker-loaded',

            "WORKER_STARTING": 'worker-starting',
            "WORKER_STARTED": 'worker-started',

            "WORKER_TERMINATING": 'worker-terminating',
            "WORKER_TERMINATED": 'worker-terminated'
        };

        // Add eventPrefix to all event types
        for (var key in Event) {
            Event[key] = 'webworker:' + Event[key];
        }

        /**
         * This documentation is for the worker script _wrapper_. Every user webworker
         * script is wrapped within this shell. All properties and methods described
         * here are attached to the `'self'` object within the worker script.
         * @class Wrapper
         * @static
         */

        /**
         * Boolean indicating whether the worker (wrapper) script has initialized.
         * @property _isInitialized
         * @type {Boolean}
         * @private
         * @default false
         */
        self._isInitialized = false;

        /**
         * Object in which all internal worker listeners are tracked.
         * @property _listeners
         * @type {Object}
         * @private
         */
        _listeners = {};
        self._listeners = _listeners;

        /**
         * Boolean indicating if the worker script has been initiated to be terminated.
         * @property _isTerminating
         * @type {Boolean}
         * @private
         * @default false
         */
        self._isTerminating = false;

        /**
         * List of pre-defined actions for the worker. This value is inherited from the
         * base class. Refer to {{#crossLink "WebWorker/Action:property"}}{{/crossLink}}
         * for more information.
         * @property Action
         * @type {Object}
         */
        self.Action = Action;

        /**
         * List of pre-defined event types for the worker. This value is inherited from the
         * base class. Refer to {{#crossLink "WebWorker/Event:property"}}{{/crossLink}}
         * for more information.
         * @property Event
         * @type {Object}
         */
        self.Event = Event;

        /**
         * This method is called by the script whenever the worker is made to terminate.
         * By default this method does nothing but it exists so that you can override it
         * with a custom function to possibly perform cleanup operations if the worker is
         * being made to terminate.
         * @method terminateHandler
         */
        self.terminateHandler = null;

        /**
         * Returns boolean indicating whether the worker (wrapper) script has initialized.
         * @method isInitialized
         * @return {Boolean} Boolean indicating whether the worker (wrapper) script has initialized.
         */
        self.isInitialized = function () {
            return self._isInitialized;
        };

        /**
         * Returns boolean indicating if the worker script has been initiated to be terminated.
         * @method isTerminating
         * @return {Boolean} Boolean indicating if the worker script has been initiated to be terminated.
         */
        self.isTerminating = function () {
            return self._isTerminating;
        };

        /**
         * The main worker script that is provided by the user is injected into this function.
         * Also any arguments passed to the {{#crossLink "Wrapper/start:method"}}{{/crossLink}} method are passed on to this function.
         * @method _main
         * @private
         * @chainable
         */
        self._main = function () {
            return self;
        };

        /**
         * This method is called when initializing the worker script.
         * It also triggers the WORKER_LOADED event on the base worker instance.
         * @method _init
         * @private
         * @chainable
         */
        self._init = function () {
            self._isInitialized = true;

            self.trigger(Event.WORKER_LOADED);

            return self;
        };

        /**
         * This method is called by the base worker instance when it's {{#crossLink "WebWorker/start:method"}}{{/crossLink}}
         * method is invoked. It also gets the arguments from the base worker.
         * Internally it calls the {{#crossLink "Wrapper/main:method"}}{{/crossLink}} method with arguments.
         * @method start
         * @chainable
         */
        self.start = function () {
            if (!self.isInitialized()) {
                return self;
            }

            self._main.apply(self, arguments);

            self.trigger(Event.WORKER_STARTED);
            return self;
        };

        /**
         * This method emulates basic jQuery `.on` event binding behavior.
         * @method on
         * @param {String} eventType The string event type that needs to be bound to.
         * @param {Function} listener The function that will listen to the event.
         * @chainable
         */
        self.on = function (eventType, listener) {
            eventType += '';
            listener = listener || null;

            if (typeof listener !== 'function') {
                return self;
            }

            if (!(eventType in _listeners)) {
                _listeners[eventType] = [];
            }

            _listeners[eventType].push(listener);
            return self;
        };

        /**
         * This method emulates basic jQuery `.one` event binding behavior.
         * @method one
         * @param {String} eventType The string event type that needs to be bound to.
         * @param {Function} listener The function that will listen to the event.
         * @chainable
         */
        self.one = function (eventType, oneListener) {
            var wrapperListener = null;

            wrapperListener = function () {
                oneListener.apply(this, arguments);
                self.off(eventType, wrapperListener);
                return;
            };

            self.on(eventType, wrapperListener);
            return;
        };

        /**
         * This method emulates basic jQuery `.off` event unbinding behavior.
         * @method off
         * @param {String} [eventType] The string event type that needs to be unbound.
         *                             If not provided or passed as `null` all event
         *                             listeners are unbound.
         * @param {Function} [listener] The function that needs to be unbound from the event.
         *                              If not provided or passed as `null` then all event
         *                              listeners for that particular event type are unbound.
         * @chainable
         */
        self.off = function (eventType, listener) {
            var key = null;

            eventType = eventType || null;
            listener = listener || null;

            if (eventType === null && listener === null) {
                for (key in _listeners) {
                    delete _listeners[key];
                }
                return self;
            }

            self._removeListenerFromEventType(eventType, listener);

            return self;
        };

        /**
         * Removes an event listener for a particular event type. Optionally if
         * the listener is not passed or is `null` it removes all listeners for
         * the specified event type.
         * @method _removeListenerFromEventType
         * @private
         * @param  {String} eventType Event type for which the listener is
         *                            being removed.
         * @param  {Function} [listener]  The listener that needs to be removed.
         *                                If not passed in or passed as `null`
         *                                all listeners for the specified event
         *                                type are removed.
         * @chainable
         */
        self._removeListenerFromEventType = function (eventType, listener) {
            var listeners = _listeners[eventType],
                i = 0;

            listener = listener || null;
            if (listener === null) {
                _listeners[eventType] = [];
                return self;
            }

            for (; i < listeners.length; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    i--;
                }
            }
            return self;
        };

        /**
         * Trigger events on the base worker instance.
         * @method trigger
         * @param  {String|Object} event The event to be triggered. Generally
         *                               expected to be a `String` but can
         *                               also be an `Object`.
         * @param  {Mixed} [data] Optional data to associate with the event.
         * @chainable
         */
        self.trigger = function (event, data) {

            var eventType = null;

            event = event || null;

            if (event === null) {
                return self;
            }

            if (typeof event === 'string') {
                eventType = event || null;
                event = {
                    "type": event
                };
            }
            else if (typeof event === 'object') {
                eventType = event.type || null;
                data = event.data;
            }

            if (eventType === null) {
                return self;
            }

            event.data = data;

            self.sendMessage(Action.TRIGGER, [event]);
            return self;
        };

        /**
         * Trigger events on the thread worker instance.
         * @method triggerSelf
         * @param  {String|Object} event The event to be triggered. Generally
         *                               expected to be a `String` but can
         *                               also be an `Object`.
         * @param  {Mixed} [data] Optional data to associate with the event.
         * @chainable
         */
        self.triggerSelf = function (event, data) {

            var self = this,
                eventType = null,
                listeners = null,
                listenersCount = null,
                listener = null,
                i = 0;

            event = event || null;

            if (event === null) {
                return self;
            }

            if (typeof event === 'string') {
                eventType = event || null;
                event = {
                    "type": event
                };
            }
            else if (typeof event === 'object') {
                eventType = event.type || null;
                data = event.data;
            }

            if (eventType === null) {
                return self;
            }

            event.data = data;

            listeners = _listeners[eventType] || null;
            if (listeners === null) {
                return self;
            }

            listenersCount = listeners.length;

            for (; i < listenersCount; i++) {
                listener = listeners[i];
                listener.apply(self, [event]);
                if (listenersCount !== listeners.length) {
                    i--;
                    listenersCount = listeners.length;
                }
            }

            return self;
        };

        /**
         * This method is used to format messages being sent between the worker
         * and the base worker instance. It's usage is similar to the
         * {{#crossLink "WebWorker/sendMessage:method"}}{{/crossLink}} method
         * in the base class.
         * @method sendMessage
         * @param {String} action The name of the method on the base worker instance to call.
         * @param {Array} args An array of arguments to pass to the action.
         * @chainable
         */
        self.sendMessage = function (action, args) {
            var message = null;

            action = action || null;
            args = args || [];

            if (action === null) {
                return self;
            }

            message = {
                "__isWebWorkerMsg": true
            };

            message.action = action;
            message.args = args;

            self.postMessage(message);

            return self;
        };

        /**
         * Terminates the worker from within the worker script.
         * Note that terminates from within the worker are asynchronous.
         * This method calls the
         * {{#crossLink "Wrapper/terminateHandler"}}{{/crossLink}},
         * if it is assigned and is a function, and then goes on to
         * communicate with the base worker class to call the method
         * {{#crossLink "WebWorker/terminateNow:method"}}{{/crossLink}}.
         *
         * The {{#crossLink "WebWorker"}}{{/crossLink}}
         * class does provide a synchronous way to terminate the worker
         * but it is not supported from within the worker script itself.
         *
         * @method terminate
         * @param {Boolean} [terminateNow] If set to `true` the worker
         *                                 also does a `self.close()`
         *                                 after it communicates the
         *                                 terminate status to the base
         *                                 worker instance.
         *                                 It is recommended to use the method
         *                                 {{#crossLink "Wrapper/terminateNow:method"}}{{/crossLink}}
         *                                 to achieve the same goal as
         *                                 it gives a more clear semantics
         *                                 in your code.
         * @chainable
         */
        self.terminate = function (terminateNow) {
            var terminateHandler = null,
                returnValue = null;

            terminateNow = !!terminateNow;
            terminateHandler = self.terminateHandler || null;

            if (!self.isTerminating()) {
                self._setTerminatingStatus(true);
                self.trigger(Event.WORKER_TERMINATING);
            }

            if (typeof terminateHandler === 'function') {
                returnValue = terminateHandler.apply(self, arguments);
            }

            self.sendMessage(Action.TERMINATE_NOW, [returnValue]);

            if (terminateNow) {
                self._nativeClose();
            }

            return self;
        };

        /**
         * Terminates the worker _immediately_ from within the worker script.
         * This method internally calls the
         * {{#crossLink "Wrapper/terminate:method"}}{{/crossLink}} method
         * to terminate the worker.
         *
         * @method terminateNow
         */
        self.terminateNow = function () {
            self.terminate(true);
            return;
        };

        /**
         * Set the terminating status. Used internally by the wrapper script
         * to keep track of the terminating status.
         * @method _setTerminatingStatus
         * @private
         * @param {Boolean} status The status you want to set.
         * @chainable
         */
        self._setTerminatingStatus = function (status) {
            self._isTerminating = status;
            return self;
        };

        /**
         * The native worker `close` method is overwritten in the wrapper to
         * give consistent behaviour with the WebWorker class.
         * This method serves as an alias for the native version of the
         * method to be used internally.
         * @method _nativeClose
         * @private
         */
        self._nativeClose = self.close;

        /**
         * The native `close` method is overwritten to serve as an alias for
         * {{#crossLink "Wrapper/terminate:method"}}{{/crossLink}}. This
         * has been done to give a consistent behaviour for the script.
         * You are encouraged to use
         * {{#crossLink "Wrapper/terminateNow:method"}}{{/crossLink}}
         * to terminate the script immediately.
         * @method close
         * @chainable
         */
        self.close = self.terminate;

        // Add a handler for the message event
        self.addEventListener('message', function (event) {
            var originalEvent = event.originalEvent || event,
                msg = originalEvent.data,
                action = null,
                args = null;

            if (typeof msg === 'object'
                && '__isWebWorkerMsg' in msg
                && msg.__isWebWorkerMsg) {
                action = msg.action;
                args = msg.args;

                self[action].apply(self, args);
                return;
            }

            return;
        }, false);

        // Initialize the worker
        self._init();

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


<commit_before>"use strict";

/**
 * main constructor for the game
 * @constructor
 */
var Solitaire = function () {

    this.el         = document.getElementById('game');

    this.balls      = this.el.querySelectorAll('.ball');
    this.balls      = Array.prototype.slice.call(this.balls);

    this.selected   = false;
    this.possibles  = [];
};

/**
 * bind all dom events
 */
Solitaire.prototype.bindEvents = function () {

    var scope = this;

    this.balls.forEach(function (ball) {

        ball.addEventListener('click', function (evt) {
            scope.click.call(scope, evt);
        }, false);
    }, this);

    // reset game
    document.getElementById('reset').addEventListener('click', function () {
        scope.reset.call(scope);
    }, false);

    // enable/disable tricky mode
    document.getElementById('tricky').addEventListener('click', function (evt) {
        scope.tricky.call(scope, evt.currentTarget);
    }, false);
};

/**
 * callback method from clicking on a ball in play
 *
 * @param evt {Event}
 */
Solitaire.prototype.click = function (evt) {

    var clicked = evt.currentTarget,
        gridPos = clicked.getAttribute('data-ball');

    // we have a ball selected
    if (this.selected) {

        // remove selected if set and clicked
        if (clicked.className.match(/selected/g)) {
            clicked.className = clicked.className.replace(/\ selected/g, '');

            this.selected = null;
            this.possibles.length = 0;

            return;
        }

        // check if in possibles array
        if (this.possibles.indexOf(gridPos) !== -1) {
            this.removeBall(gridPos);
        }

    } else {

        // check for a ball in the hole
        if (clicked.className.match(/empty/g)) {
            return;
        }

        clicked.className = clicked.className + ' selected';

        this.selected = clicked;

        this.getPossibles(gridPos);
    }
};

/**
 * get all possibles positions that the user could click next
 *
 * @param gridPos {string}
 */
Solitaire.prototype.getPossibles = function (gridPos) {

    var splits = gridPos.split('-'),
        x = parseFloat(splits[0]),
        y = parseFloat(splits[1]),
        xConstraints = [0, 8],
        yConstraints = [0, 8];

    this.possibles.length = 0;

    // change constraints based on pos (narrow three in a row)
    if (y < 3 || y > 5) {
        xConstraints = [2, 6];
    }

    if (x < 3 || x > 5) {
        yConstraints = [2, 6];
    }

    // check left
    if ((x - 2) > xConstraints[0]) {
        this.addPossible((x - 2) + '-' + y);
    }

    // check right
    if ((x + 2) < xConstraints[1]) {
        this.addPossible((x + 2) + '-' + y);
    }

    // check top
    if ((y - 2) > yConstraints[0]) {
        this.addPossible(x + '-' + (y - 2));
    }

    // check bottom
    if ((y + 2) < yConstraints[1]) {
        this.addPossible(x + '-' + (y + 2));
    }
};

/**
 * get the ball reference dom object based on identifier
 *
 * @param identifier {string}
 */
Solitaire.prototype.getBall = function (identifier) {

    return this.balls.find(function (ball) {
        return ball.getAttribute('data-ball') === identifier;
    });
};

/**
 * add a possible position to the possibles array if not empty
 * @param identifier
 */
Solitaire.prototype.addPossible = function (identifier) {

    var target = this.getBall(identifier);

    if (target && target.className.match(/empty/g)) {
        this.possibles.push(identifier);
    }
};

/**
 * removes the ball between the two action spots
 *
 * @param gridPos {string}
 * @todo re-write - insane to follow logic
 */
Solitaire.prototype.removeBall = function (gridPos) {

    var selectedId = this.selected.getAttribute('data-ball').split('-'),
        targetId = gridPos.split('-'),
        identifier,
        target;

    // both on the x axis, sanity check the ball is in the middle
    if (selectedId[0] === targetId[0]) {
        if (selectedId[1] > targetId[1]) {
            identifier = (targetId[0] + '-' + (parseFloat(targetId[1]) + 1));
        } else {
            identifier = (targetId[0] + '-' + (parseFloat(targetId[1]) - 1));
        }
    }

    // both on the y axis, sanity check the ball is in the middle
    if (selectedId[1] === targetId[1]) {
        if (selectedId[0] > targetId[0]) {
            identifier = ((parseFloat(targetId[0]) + 1) + '-' + targetId[1]);
        } else {
            identifier = ((parseFloat(targetId[0]) - 1) + '-' + targetId[1]);
        }
    }

    target = this.getBall(identifier);

    // if empty - bomb out
    if (target.className.match(/empty/g)) {
        return;
    }

    // place ball in new position
    this.getBall(gridPos).className = 'ball';

    // empty target
    target.className = target.className + ' empty';

    // null-ify selected
    this.selected.className = this.selected.className.replace(/selected/g, 'empty');
    this.selected = null;
};

/**
 * reset game board
 */
Solitaire.prototype.reset = function () {

    this.balls.map(function (ball) {
        ball.className = 'ball';
    });

    this.getBall('4-4').className = 'ball empty';
};

/**
 * enable a tricky mode on the game
 */
Solitaire.prototype.tricky = function (button) {

    if (this.el.className.match(/tricky/g)) {
        this.el.className = 'game';
        button.className = button.className.replace(/( )?on/g, '');
    } else {
        this.el.className = 'game tricky';
        button.className = button.className + ' on';
    }
};

// start game ;)
var game = new Solitaire();

// bind events
game.bindEvents();
<commit_msg>Removing state from the DOM<commit_after>"use strict";

/**
 * main constructor for the game
 * @constructor
 */
var Solitaire = function () {

    this.el         = document.getElementById('game');

    this.balls      = this.el.querySelectorAll('.ball');
    this.balls      = Array.prototype.slice.call(this.balls);

    this.selected   = false;
    this.possibles  = [];

    this.trickyMode = false;
};

/**
 * bind all dom events
 */
Solitaire.prototype.bindEvents = function () {

    var scope = this;

    this.balls.forEach(function (ball) {

        ball.addEventListener('click', function (evt) {
            scope.click.call(scope, evt);
        }, false);
    }, this);

    // reset game
    document.getElementById('reset').addEventListener('click', function () {
        scope.reset.call(scope);
    }, false);

    // enable/disable tricky mode
    document.getElementById('tricky').addEventListener('click', function (evt) {
        scope.tricky.call(scope, evt.currentTarget);
    }, false);
};

/**
 * callback method from clicking on a ball in play
 *
 * @param evt {Event}
 */
Solitaire.prototype.click = function (evt) {

    var clicked = evt.currentTarget,
        gridPos = clicked.getAttribute('data-ball');

    // we have a ball selected
    if (this.selected) {

        // remove selected if set and clicked
        if (clicked.className.match(/selected/g)) {
            clicked.className = clicked.className.replace(/\ selected/g, '');

            this.selected = null;
            this.possibles.length = 0;

            return;
        }

        // check if in possibles array
        if (this.possibles.indexOf(gridPos) !== -1) {
            this.removeBall(gridPos);
        }

    } else {

        // check for a ball in the hole
        if (clicked.className.match(/empty/g)) {
            return;
        }

        clicked.className = clicked.className + ' selected';

        this.selected = clicked;

        this.getPossibles(gridPos);
    }
};

/**
 * get all possibles positions that the user could click next
 *
 * @param gridPos {string}
 */
Solitaire.prototype.getPossibles = function (gridPos) {

    var splits = gridPos.split('-'),
        x = parseFloat(splits[0]),
        y = parseFloat(splits[1]),
        xConstraints = [0, 8],
        yConstraints = [0, 8];

    this.possibles.length = 0;

    // change constraints based on pos (narrow three in a row)
    if (y < 3 || y > 5) {
        xConstraints = [2, 6];
    }

    if (x < 3 || x > 5) {
        yConstraints = [2, 6];
    }

    // check left
    if ((x - 2) > xConstraints[0]) {
        this.addPossible((x - 2) + '-' + y);
    }

    // check right
    if ((x + 2) < xConstraints[1]) {
        this.addPossible((x + 2) + '-' + y);
    }

    // check top
    if ((y - 2) > yConstraints[0]) {
        this.addPossible(x + '-' + (y - 2));
    }

    // check bottom
    if ((y + 2) < yConstraints[1]) {
        this.addPossible(x + '-' + (y + 2));
    }
};

/**
 * get the ball reference dom object based on identifier
 *
 * @param identifier {string}
 */
Solitaire.prototype.getBall = function (identifier) {

    return this.balls.find(function (ball) {
        return ball.getAttribute('data-ball') === identifier;
    });
};

/**
 * add a possible position to the possibles array if not empty
 * @param identifier
 */
Solitaire.prototype.addPossible = function (identifier) {

    var target = this.getBall(identifier);

    if (target && target.className.match(/empty/g)) {
        this.possibles.push(identifier);
    }
};

/**
 * removes the ball between the two action spots
 *
 * @param gridPos {string}
 * @todo re-write - insane to follow logic
 */
Solitaire.prototype.removeBall = function (gridPos) {

    var selectedId = this.selected.getAttribute('data-ball').split('-'),
        targetId = gridPos.split('-'),
        identifier,
        target;

    // both on the x axis, sanity check the ball is in the middle
    if (selectedId[0] === targetId[0]) {
        if (selectedId[1] > targetId[1]) {
            identifier = (targetId[0] + '-' + (parseFloat(targetId[1]) + 1));
        } else {
            identifier = (targetId[0] + '-' + (parseFloat(targetId[1]) - 1));
        }
    }

    // both on the y axis, sanity check the ball is in the middle
    if (selectedId[1] === targetId[1]) {
        if (selectedId[0] > targetId[0]) {
            identifier = ((parseFloat(targetId[0]) + 1) + '-' + targetId[1]);
        } else {
            identifier = ((parseFloat(targetId[0]) - 1) + '-' + targetId[1]);
        }
    }

    target = this.getBall(identifier);

    // if empty - bomb out
    if (target.className.match(/empty/g)) {
        return;
    }

    // place ball in new position
    this.getBall(gridPos).className = 'ball';

    // empty target
    target.className = target.className + ' empty';

    // null-ify selected
    this.selected.className = this.selected.className.replace(/selected/g, 'empty');
    this.selected = null;
};

/**
 * reset game board
 */
Solitaire.prototype.reset = function () {

    this.balls.map(function (ball) {
        ball.className = 'ball';
    });

    this.getBall('4-4').className = 'ball empty';
};

/**
 * enable a tricky mode on the game
 */
Solitaire.prototype.tricky = function (button) {

    if (this.trickyMode) {
        this.el.className = 'game';
        button.className = button.className.replace(/( )?on/g, '');
    } else {
        this.el.className = 'game tricky';
        button.className = button.className + ' on';
    }

    this.trickyMode = !this.trickyMode;
};

// start game ;)
var game = new Solitaire();

// bind events
game.bindEvents();
<|endoftext|>


<commit_before>import stringArgv from 'string-argv';
import { isNumber, toInteger, toNumber } from 'lodash/lang';
import { endsWith, trimEnd } from 'lodash/string';
import CommandParserError from '../Errors/CommandParserError';

/**
 * Parses the signature of a command.
 */
export default class Parser {
  /**
   * Constants for special tokens in command signatures.
   * @type {Object}
   * @const
   */
  static DELIMITERS = {
    DESCRIPTION: ':',
    PARAMETER_TYPE: '>>',
  };

  /**
   * Some constants for command arguments.
   * @type {Object}
   * @const
   */
  static TOKEN = {
    ARITY: {
      UNARY: 'UNARY',
      VARIADIC: 'VARIADIC',
    },
    TYPE: {
      BOOLEAN: 'BOOLEAN',
      BOOL: 'BOOLEAN',
      INTEGER: 'INTEGER',
      INT: 'INTEGER',
      NUMBER: 'NUMBER',
      NUM: 'NUMBER',
      STRING: 'STRING',
      STR: 'STRING',
    },
  };

  /**
   * Maps parameter types to type checking functions.
   * @type {Object}
   */
  static TYPE_CHECKERS = {
    [Parser.TOKEN.TYPE.BOOLEAN]: (value) => {
      const lower = value.toLowerCase();

      return lower === 'true' || lower === 'false';
    },
    [Parser.TOKEN.TYPE.INTEGER]: (value) => !isNaN(value) && isNumber(+value),
    [Parser.TOKEN.TYPE.NUMBER]: (value) => !isNaN(value) && isNumber(+value),
  };

  /**
   * Maps parameter types to type conversion functions.
   * @type {Object}
   */
  static TYPE_CONVERTERS = {
    [Parser.TOKEN.TYPE.BOOLEAN]: value => value === 'true',
    [Parser.TOKEN.TYPE.INTEGER]: toInteger,
    [Parser.TOKEN.TYPE.NUMBER]: toNumber,
  };

  /**
   * Parses a given command signature.
   * @param {String} signature - The command signature.
   * @returns {Object} Object containing data on the command signature.
   * @throws {CommandParserError}
   */
  static parse(signature) {
    const trimmed = signature.trim();

    if (!trimmed) {
      throw new CommandParserError('Signature cannot be empty.');
    }

    const partition = trimmed.split(' ');
    const identifier = partition[0].trim();

    if (partition.length > 1) {
      const parameterString = partition.slice(1).join(' ').trim();
      const parameterRegex = /\[\s*(.*?)\s*\]/g;
      const matches = [];
      let match;

      while (match = parameterRegex.exec(parameterString)) {
        matches.push(match[1]);
      }

      if (parameterString && !matches.length) {
        throw new CommandParserError(`Expected parameter definitions after command name but found none. Given signature: <${signature}>.`);
      }

      return {
        identifier,
        parameters: Parser.parseParameters(matches),
      };
    }

    return {
      identifier,
    };
  }

  /**
   * Parses the parameters in a command signature.
   * @param {Array.<String>} parameters - Array of parameters parsed from the signature.
   * @returns {Array.<Object>} Array containing data on the parsed parameters.
   * @throws {CommandParserError}
   */
  static parseParameters(parameters) {
    return parameters.reduce(
      (previous, current, index) => {
        const token = Parser.parseParameter(current);

        if (previous.seen.parameterNames[token.name]) {
          throw new CommandParserError(`Encountered duplicate parameter names: ${token.name}.`);
        } else if (token.arity === Parser.TOKEN.ARITY.VARIADIC && index < parameters.length - 1) {
          throw new CommandParserError(`Parameter of type array can only appear at the end of the command signature. Given parameters: <[${parameters.join('] [')}]>.`);
        } else if (!token.optional && previous.seen.optional) {
          throw new CommandParserError(`Encountered required parameter after optional parameter: ${token.name}.`);
        }

        return {
          seen: {
            optional: previous.seen.optional || token.optional,
            parameterNames: {
              ...previous.seen.parameterNames,
              [token.name]: true,
            },
          },
          parsedParameters: [...previous.parsedParameters, token],
        };
      },
      {
        seen: {
          optional: false,
          parameterNames: {},
        },
        parsedParameters: [],
      },
    ).parsedParameters;
  }

  /**
   * Parses a single command parameter.
   * @param {String} parameter - The parameter.
   * @returns {Object} Object containing data on the parsed parameter.
   * @throws {CommandParserError}
   */
  static parseParameter(parameter) {
    const token = {
      name: null,
      description: null,
      arity: Parser.TOKEN.ARITY.UNARY,
      type: Parser.TOKEN.TYPE.STRING,
      optional: false,
      defaultValue: null,
    };

    const signatureAndDescription = parameter.split(Parser.DELIMITERS.DESCRIPTION);
    let signature;

    if (signatureAndDescription.length > 1) {
      signature = signatureAndDescription[0].trim();
      token.description = signatureAndDescription.slice(1).join(Parser.DELIMITERS.DESCRIPTION).trim();
    } else {
      signature = parameter;
    }

    const typeAndSignature = signature.split(Parser.DELIMITERS.PARAMETER_TYPE);

    if (typeAndSignature.length > 1) {
      const type = typeAndSignature[0].toUpperCase().trim();

      if (!Parser.TOKEN.TYPE[type]) {
        throw new CommandParserError(`${type} is not a valid parameter type. Given parameter: <[${parameter}]>.`);
      }

      token.type = Parser.TOKEN.TYPE[type];
      signature = typeAndSignature.slice(1).join(Parser.DELIMITERS.PARAMETER_TYPE).trim();
    }

    const matches = signature.match(/(.+)=(.+)/);

    if (matches) {
      signature = matches[1].trim();
      token.defaultValue = matches[2].trim();
      token.optional = true;
    }

    if (endsWith(signature, '?')) {
      token.optional = true;
      signature = trimEnd(signature, ' ?');
    }

    if (endsWith(signature, '*')) {
      token.arity = Parser.TOKEN.ARITY.VARIADIC;
      token.defaultValue = token.defaultValue && stringArgv(token.defaultValue);
      signature = trimEnd(signature, ' *');
    }

    if (token.defaultValue && token.type !== Parser.TOKEN.TYPE.STRING) {
      const typeValidator = (value) => {
        if (!Parser.TYPE_CHECKERS[token.type](value)) {
          throw new CommandParserError(`Expected default value of type <${token.type}>, got <${typeof value}>. Given parameter: <[${parameter}]>.`);
        }

        return value;
      };

      if (token.arity === Parser.TOKEN.ARITY.UNARY) {
        token.defaultValue = Parser.TYPE_CONVERTERS[token.type](typeValidator(token.defaultValue));
      } else {
        token.defaultValue = token.defaultValue.map(value => (
          Parser.TYPE_CONVERTERS[token.type](typeValidator(value))
        ));
      }
    }

    token.name = signature;

    return token;
  }
}
<commit_msg>Modify the error message for parameter type.<commit_after>import stringArgv from 'string-argv';
import { isNumber, toInteger, toNumber } from 'lodash/lang';
import { endsWith, trimEnd } from 'lodash/string';
import CommandParserError from '../Errors/CommandParserError';

/**
 * Parses the signature of a command.
 */
export default class Parser {
  /**
   * Constants for special tokens in command signatures.
   * @type {Object}
   * @const
   */
  static DELIMITERS = {
    DESCRIPTION: ':',
    PARAMETER_TYPE: '>>',
  };

  /**
   * Some constants for command arguments.
   * @type {Object}
   * @const
   */
  static TOKEN = {
    ARITY: {
      UNARY: 'UNARY',
      VARIADIC: 'VARIADIC',
    },
    TYPE: {
      BOOLEAN: 'BOOLEAN',
      BOOL: 'BOOLEAN',
      INTEGER: 'INTEGER',
      INT: 'INTEGER',
      NUMBER: 'NUMBER',
      NUM: 'NUMBER',
      STRING: 'STRING',
      STR: 'STRING',
    },
  };

  /**
   * Maps parameter types to type checking functions.
   * @type {Object}
   */
  static TYPE_CHECKERS = {
    [Parser.TOKEN.TYPE.BOOLEAN]: (value) => {
      const lower = value.toLowerCase();

      return lower === 'true' || lower === 'false';
    },
    [Parser.TOKEN.TYPE.INTEGER]: (value) => !isNaN(value) && isNumber(+value),
    [Parser.TOKEN.TYPE.NUMBER]: (value) => !isNaN(value) && isNumber(+value),
  };

  /**
   * Maps parameter types to type conversion functions.
   * @type {Object}
   */
  static TYPE_CONVERTERS = {
    [Parser.TOKEN.TYPE.BOOLEAN]: value => value === 'true',
    [Parser.TOKEN.TYPE.INTEGER]: toInteger,
    [Parser.TOKEN.TYPE.NUMBER]: toNumber,
  };

  /**
   * Parses a given command signature.
   * @param {String} signature - The command signature.
   * @returns {Object} Object containing data on the command signature.
   * @throws {CommandParserError}
   */
  static parse(signature) {
    const trimmed = signature.trim();

    if (!trimmed) {
      throw new CommandParserError('Signature cannot be empty.');
    }

    const partition = trimmed.split(' ');
    const identifier = partition[0].trim();

    if (partition.length > 1) {
      const parameterString = partition.slice(1).join(' ').trim();
      const parameterRegex = /\[\s*(.*?)\s*\]/g;
      const matches = [];
      let match;

      while (match = parameterRegex.exec(parameterString)) {
        matches.push(match[1]);
      }

      if (parameterString && !matches.length) {
        throw new CommandParserError(`Expected parameter definitions after command name but found none. Given signature: <${signature}>.`);
      }

      return {
        identifier,
        parameters: Parser.parseParameters(matches),
      };
    }

    return {
      identifier,
    };
  }

  /**
   * Parses the parameters in a command signature.
   * @param {Array.<String>} parameters - Array of parameters parsed from the signature.
   * @returns {Array.<Object>} Array containing data on the parsed parameters.
   * @throws {CommandParserError}
   */
  static parseParameters(parameters) {
    return parameters.reduce(
      (previous, current, index) => {
        const token = Parser.parseParameter(current);

        if (previous.seen.parameterNames[token.name]) {
          throw new CommandParserError(`Encountered duplicate parameter names: ${token.name}.`);
        } else if (token.arity === Parser.TOKEN.ARITY.VARIADIC && index < parameters.length - 1) {
          throw new CommandParserError(`Parameter of type array can only appear at the end of the command signature. Given parameters: <[${parameters.join('] [')}]>.`);
        } else if (!token.optional && previous.seen.optional) {
          throw new CommandParserError(`Encountered required parameter after optional parameter: ${token.name}.`);
        }

        return {
          seen: {
            optional: previous.seen.optional || token.optional,
            parameterNames: {
              ...previous.seen.parameterNames,
              [token.name]: true,
            },
          },
          parsedParameters: [...previous.parsedParameters, token],
        };
      },
      {
        seen: {
          optional: false,
          parameterNames: {},
        },
        parsedParameters: [],
      },
    ).parsedParameters;
  }

  /**
   * Parses a single command parameter.
   * @param {String} parameter - The parameter.
   * @returns {Object} Object containing data on the parsed parameter.
   * @throws {CommandParserError}
   */
  static parseParameter(parameter) {
    const token = {
      name: null,
      description: null,
      arity: Parser.TOKEN.ARITY.UNARY,
      type: Parser.TOKEN.TYPE.STRING,
      optional: false,
      defaultValue: null,
    };

    const signatureAndDescription = parameter.split(Parser.DELIMITERS.DESCRIPTION);
    let signature;

    if (signatureAndDescription.length > 1) {
      signature = signatureAndDescription[0].trim();
      token.description = signatureAndDescription.slice(1).join(Parser.DELIMITERS.DESCRIPTION).trim();
    } else {
      signature = parameter;
    }

    const typeAndSignature = signature.split(Parser.DELIMITERS.PARAMETER_TYPE);

    if (typeAndSignature.length > 1) {
      const type = typeAndSignature[0].toUpperCase().trim();

      if (!Parser.TOKEN.TYPE[type]) {
        throw new CommandParserError(`${type} is not a valid parameter type. Given parameter: <[${parameter}]>.`);
      }

      token.type = Parser.TOKEN.TYPE[type];
      signature = typeAndSignature.slice(1).join(Parser.DELIMITERS.PARAMETER_TYPE).trim();
    }

    const matches = signature.match(/(.+)=(.+)/);

    if (matches) {
      signature = matches[1].trim();
      token.defaultValue = matches[2].trim();
      token.optional = true;
    }

    if (endsWith(signature, '?')) {
      token.optional = true;
      signature = trimEnd(signature, ' ?');
    }

    if (endsWith(signature, '*')) {
      token.arity = Parser.TOKEN.ARITY.VARIADIC;
      token.defaultValue = token.defaultValue && stringArgv(token.defaultValue);
      signature = trimEnd(signature, ' *');
    }

    if (token.defaultValue && token.type !== Parser.TOKEN.TYPE.STRING) {
      const typeValidator = (value) => {
        if (!Parser.TYPE_CHECKERS[token.type](value)) {
          throw new CommandParserError(`Expected default value of type <${token.type}>. Given parameter: <[${parameter}]>.`);
        }

        return value;
      };

      if (token.arity === Parser.TOKEN.ARITY.UNARY) {
        token.defaultValue = Parser.TYPE_CONVERTERS[token.type](typeValidator(token.defaultValue));
      } else {
        token.defaultValue = token.defaultValue.map(value => (
          Parser.TYPE_CONVERTERS[token.type](typeValidator(value))
        ));
      }
    }

    token.name = signature;

    return token;
  }
}
<|endoftext|>