<commit_before>// @flow
import React, { Component } from 'react';
import { StyleSheet, View, ScrollView, RefreshControl, TouchableOpacity, Text } from 'react-native';
import { connect } from 'react-redux';
import { TrelloCard, MemberIcon } from 'DailyScrum/src/components';
import { teamSelector } from 'DailyScrum/src/modules/sprints/reducer';
import { currentUserSelector } from 'DailyScrum/src/modules/users/reducer';
import type { CardType } from 'DailyScrum/src/modules/cards/reducer';
import type { ScrumbleTeamMemberType, ScrumbleTeamType } from 'DailyScrum/src/types/Scrumble/common';
import { roundToDecimalPlace } from '../services/MathService';
import type { UserType } from '../types';

class CardsList extends Component {
  props: PropsType;
  state: StateType = { filterableMembers: [], filteredMember: null, isRefreshing: false };

  // Use FlatList / SectionList when 0.43 out

  // TODO identify unchecked checklists
  // TODO extract and show post estimated points
  // TODO show labels
  // TODO on click show attachments
  // TODO on click show description

  componentDidMount() {
    const filteredMember = this.props.user ? this.props.user.id : null;
    this.setState({ filterableMembers: this.getFilterableMembers(), filteredMember });
  }

  handleRefresh = () => {
    this.setState({ isRefreshing: true }, () => {
      this.props.onRefresh().then(() => {
        this.setState({ isRefreshing: false, filterableMembers: this.getFilterableMembers(), filteredMember: null });
      });
    });
  };

  getFilterableMembers = () => {
    let members = {};
    for (let card of this.props.cards) {
      for (let member of card.idMembers) {
        members[member] = true;
      }
    }
    return Object.keys(members);
  };

  filterMember = (member: ScrumbleTeamMemberType) => {
    this.setState({ filteredMember: this.state.filteredMember !== member ? member : null });
  };

  renderFilterableMember = (memberId: string) => {
    const { cards, team } = this.props;
    const member = team && team.find(teamMember => teamMember && teamMember.id === memberId);
    if (!member) return;

    return (
      <View key={memberId} style={styles.filterableMemberContainer}>
        <TouchableOpacity onPress={() => this.filterMember(memberId)}>
          <View style={this.state.filteredMember && this.state.filteredMember !== memberId && { opacity: 0.6 }}>
            <MemberIcon member={member} />
          </View>
        </TouchableOpacity>
        <Text style={styles.filterableMemberPoints}>
          {cards
            ? cards
                .filter(card => card.idMembers.includes(memberId))
                .reduce(
                  (total: number, card: CardType) =>
                    total + (card.points ? roundToDecimalPlace(card.points / card.idMembers.length) : 0),
                  0
                )
            : 0}
        </Text>
      </View>
    );
  };

  render() {
    const { cards, team } = this.props;
    if (!cards.length) return <View style={this.props.style}><Text style={styles.noCardsText}>No cards yet</Text></View>;
    return (
      <ScrollView
        contentContainerStyle={this.props.style}
        showsVerticalScrollIndicator={false}
        refreshControl={<RefreshControl refreshing={this.state.isRefreshing} onRefresh={this.handleRefresh} />}
      >
        <View style={styles.filterContainer}>
          {team && this.state.filterableMembers.map(memberId => this.renderFilterableMember(memberId))}
        </View>
        {cards &&
          cards
            .filter(card => this.state.filteredMember ? card.idMembers.includes(this.state.filteredMember) : true)
            .map(card => <View key={card.idShort}><TrelloCard card={card} /></View>)}
      </ScrollView>
    );
  }
}

const styles = StyleSheet.create({
  noCardsText: {
    textAlign: 'center',
  },
  filterContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    marginBottom: 10,
  },
  filterableMemberPoints: {
    marginTop: 4,
    textAlign: 'center',
    fontWeight: 'bold',
    color: '#006580',
  },
});

type StateType = {
  isRefreshing: boolean,
  filterableMembers: string[],
  filteredMember: ?string,
};

type PropsType = {
  style?: any,
  cards: CardType[],
  onRefresh: Function,
  team: ?ScrumbleTeamType,
  user: ?UserType,
};

const mapStateToProps = state => ({
  team: teamSelector(state),
  user: currentUserSelector(state),
});

export default connect(mapStateToProps)(CardsList);
<commit_msg>[CardsList] Don't filter by myself by default if I'm not filterable<commit_after>// @flow
import React, { Component } from 'react';
import { StyleSheet, View, ScrollView, RefreshControl, TouchableOpacity, Text } from 'react-native';
import { connect } from 'react-redux';
import { TrelloCard, MemberIcon } from 'DailyScrum/src/components';
import { teamSelector } from 'DailyScrum/src/modules/sprints/reducer';
import { currentUserSelector } from 'DailyScrum/src/modules/users/reducer';
import type { CardType } from 'DailyScrum/src/modules/cards/reducer';
import type { ScrumbleTeamMemberType, ScrumbleTeamType } from 'DailyScrum/src/types/Scrumble/common';
import { roundToDecimalPlace } from '../services/MathService';
import type { UserType } from '../types';

class CardsList extends Component {
  props: PropsType;
  state: StateType = { filterableMembers: [], filteredMember: null, isRefreshing: false };

  // Use FlatList / SectionList when 0.43 out

  // TODO identify unchecked checklists
  // TODO extract and show post estimated points
  // TODO show labels
  // TODO on click show attachments
  // TODO on click show description

  componentDidMount() {
    const filterableMembers = this.getFilterableMembers();
    const filteredMember = this.props.user && filterableMembers.includes(this.props.user.id)
      ? this.props.user.id
      : null;
    this.setState({ filterableMembers, filteredMember });
  }

  handleRefresh = () => {
    this.setState({ isRefreshing: true }, () => {
      this.props.onRefresh().then(() => {
        this.setState({ isRefreshing: false, filterableMembers: this.getFilterableMembers(), filteredMember: null });
      });
    });
  };

  getFilterableMembers = () => {
    let members = {};
    for (let card of this.props.cards) {
      for (let member of card.idMembers) {
        members[member] = true;
      }
    }
    return Object.keys(members);
  };

  filterMember = (member: ScrumbleTeamMemberType) => {
    this.setState({ filteredMember: this.state.filteredMember !== member ? member : null });
  };

  renderFilterableMember = (memberId: string) => {
    const { cards, team } = this.props;
    const member = team && team.find(teamMember => teamMember && teamMember.id === memberId);
    if (!member) return;

    return (
      <View key={memberId} style={styles.filterableMemberContainer}>
        <TouchableOpacity onPress={() => this.filterMember(memberId)}>
          <View style={this.state.filteredMember && this.state.filteredMember !== memberId && { opacity: 0.6 }}>
            <MemberIcon member={member} />
          </View>
        </TouchableOpacity>
        <Text style={styles.filterableMemberPoints}>
          {cards
            ? cards
                .filter(card => card.idMembers.includes(memberId))
                .reduce(
                  (total: number, card: CardType) =>
                    total + (card.points ? roundToDecimalPlace(card.points / card.idMembers.length) : 0),
                  0
                )
            : 0}
        </Text>
      </View>
    );
  };

  render() {
    const { cards, team } = this.props;
    if (!cards.length)
      return <View style={this.props.style}><Text style={styles.noCardsText}>No cards yet</Text></View>;
    return (
      <ScrollView
        contentContainerStyle={this.props.style}
        showsVerticalScrollIndicator={false}
        refreshControl={<RefreshControl refreshing={this.state.isRefreshing} onRefresh={this.handleRefresh} />}
      >
        <View style={styles.filterContainer}>
          {team && this.state.filterableMembers.map(memberId => this.renderFilterableMember(memberId))}
        </View>
        {cards &&
          cards
            .filter(card => this.state.filteredMember ? card.idMembers.includes(this.state.filteredMember) : true)
            .map(card => <View key={card.idShort}><TrelloCard card={card} /></View>)}
      </ScrollView>
    );
  }
}

const styles = StyleSheet.create({
  noCardsText: {
    textAlign: 'center',
  },
  filterContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    marginBottom: 10,
  },
  filterableMemberPoints: {
    marginTop: 4,
    textAlign: 'center',
    fontWeight: 'bold',
    color: '#006580',
  },
});

type StateType = {
  isRefreshing: boolean,
  filterableMembers: string[],
  filteredMember: ?string,
};

type PropsType = {
  style?: any,
  cards: CardType[],
  onRefresh: Function,
  team: ?ScrumbleTeamType,
  user: ?UserType,
};

const mapStateToProps = state => ({
  team: teamSelector(state),
  user: currentUserSelector(state),
});

export default connect(mapStateToProps)(CardsList);
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
// components
import DefaultArea from './DefaultArea';
import SelectionArea from './SelectionArea';
import InstructionsArea from './InstructionsArea';
import EditVerseArea from './EditVerseArea';
import CommentArea from './CommentArea';
import style from '../css/Style';
class CheckArea extends Component {
  render() {
    const {
      actions,
      mode,
      tags,
      verseText,
      verseChanged,
      comment,
      contextIdReducer,
      selectionsReducer,
      projectDetailsReducer
    } = this.props;
    let modeArea;
    switch (mode) {
      case 'edit':
        modeArea = (
          <EditVerseArea
            tags={tags}
            verseText={verseText}
            verseChanged={verseChanged}
            actions={actions}
            dir={projectDetailsReducer.manifest.target_language.direction}
          />
        );
        break;
      case 'comment':
        modeArea = <CommentArea comment={comment} actions={actions} />;
        break;
      case 'select':
        modeArea = (

            <InstructionsArea
              verseText={verseText}
              selectionsReducer={selectionsReducer}
              quote={contextIdReducer.contextId.quote}
              mode={mode}
            />

);
        break;
      case 'default':
      default:
        modeArea = (

            <InstructionsArea dontShowTranslation={true} verseText={verseText} selectionsReducer={selectionsReducer} quote={contextIdReducer.contextId.quote} />

        );
    }

    return (

        {mode === 'select' ?
          <SelectionArea
            {...this.props}
            actions={actions}
            quote={contextIdReducer.contextId.quote}
          /> :
          <DefaultArea {...this.props} />}

          {modeArea}

    );
  }
}

CheckArea.propTypes = {
  actions: PropTypes.object.isRequired,
  mode: PropTypes.string.isRequired,
  tags: PropTypes.array.isRequired,
  verseText: PropTypes.string.isRequired,
  verseChanged: PropTypes.bool.isRequired,
  comment: PropTypes.string.isRequired,
  contextIdReducer: PropTypes.shape({
    contextId: PropTypes.object
  }).isRequired,
  selectionsReducer: PropTypes.shape({
    selections: PropTypes.array
  }).isRequired,
  projectDetailsReducer: PropTypes.shape({
    manifest: PropTypes.object
  }).isRequired
};

export default CheckArea;
<commit_msg>added gl quote to props for render<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
// components
import DefaultArea from './DefaultArea';
import SelectionArea from './SelectionArea';
import InstructionsArea from './InstructionsArea';
import EditVerseArea from './EditVerseArea';
import CommentArea from './CommentArea';
import style from '../css/Style';
class CheckArea extends Component {
  render() {
    const {
      actions,
      mode,
      tags,
      verseText,
      verseChanged,
      comment,
      glQuote,
      selectionsReducer,
      projectDetailsReducer
    } = this.props;
    let modeArea;
    switch (mode) {
      case 'edit':
        modeArea = (
          <EditVerseArea
            tags={tags}
            verseText={verseText}
            verseChanged={verseChanged}
            actions={actions}
            dir={projectDetailsReducer.manifest.target_language.direction}
          />
        );
        break;
      case 'comment':
        modeArea = <CommentArea comment={comment} actions={actions} />;
        break;
      case 'select':
        modeArea = (

            <InstructionsArea
              verseText={verseText}
              selectionsReducer={selectionsReducer}
              quote={glQuote}
              mode={mode}
            />

);
        break;
      case 'default':
      default:
        modeArea = (

            <InstructionsArea dontShowTranslation={true} verseText={verseText} selectionsReducer={selectionsReducer} quote={glQuote} />

        );
    }

    return (

        {mode === 'select' ?
          <SelectionArea
            {...this.props}
            actions={actions}
            quote={glQuote}
          /> :
          <DefaultArea {...this.props} />}

          {modeArea}

    );
  }
}

CheckArea.propTypes = {
  actions: PropTypes.object.isRequired,
  mode: PropTypes.string.isRequired,
  tags: PropTypes.array.isRequired,
  verseText: PropTypes.string.isRequired,
  verseChanged: PropTypes.bool.isRequired,
  comment: PropTypes.string.isRequired,
  glQuote: PropTypes.string.isRequired,
  contextIdReducer: PropTypes.shape({
    contextId: PropTypes.object
  }).isRequired,
  selectionsReducer: PropTypes.shape({
    selections: PropTypes.array
  }).isRequired,
  projectDetailsReducer: PropTypes.shape({
    manifest: PropTypes.object
  }).isRequired
};

export default CheckArea;
<|endoftext|>


<commit_before>// the eslint "no-unused-expressions" rule is triggered by the `expect`
// assertions in the tests. So, in order to avoid problems with eslint, I'm
// disabling that rule in this file
//
/* eslint-disable no-unused-expressions */

import React from 'react';
import { shallow, render, mount } from 'enzyme';
import chai from 'chai';
import Form from './Form';

const expect = chai.expect;

const debug = (component) => {
    console.log('------------------------------------------------------------');
    console.log(component.debug());
    console.log('------------------------------------------------------------');
};

/* *****************************************************************************
the Form component
    should be a <form> element
*/
describe('the Form component', () => {

    it('should be a <form> element', () => {
        const component = shallow(<Form />);

        expect(component.is('form')).to.equal(true);
    });

});

/* *****************************************************************************
a Form component with a TextInput element
    should include a <Form.TextInput> as a child
*/
describe('a Form component with a TextInput element', () => {

    it('should include a <Form.TextInput> as a child', () => {
        const component = shallow(
            <Form>
                <Form.TextInput />
            </Form>
        );

        expect(component.find(Form.TextInput)).to.have.length(1);
    });
});

/* *****************************************************************************
when initializing a Form with a required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when initializing a Form with a required TextInput', () => {

    const required = true;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when initializing a Form with a non-required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when initializing a Form with a non-required TextInput', () => {

    const required = false;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing the value of a required TextInput (but not blurring)
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when changing the value of a required TextInput (but not blurring)', () => {

    const required = true;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing the value of a non-required TextInput (but not blurring)
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when changing the value of a non-required TextInput (but not blurring)', () => {

    const required = false;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing (and blurring) the value of a required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message SHOULD be displayed with an invalid value
    the component validation message SHOULD be displayed with an invalid value
*/
describe('when changing (and blurring) the value of a required TextInput', () => {

    const required = true;

    it('the global validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'valid';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'valid';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the global validation message SHOULD be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(1, 'Alert');
    });

    it('the component validation message SHOULD be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        // debug(component);
        expect(component.find('.has-error')).to.have.length(1, 'has-error');
        expect(component.find('.help-block')).to.have.length(1, 'help-block');
    });
});
<commit_msg>added specs for default & custom error messages<commit_after>// the eslint "no-unused-expressions" rule is triggered by the `expect`
// assertions in the tests. So, in order to avoid problems with eslint, I'm
// disabling that rule in this file
//
/* eslint-disable no-unused-expressions */

import React from 'react';
import { shallow, render, mount } from 'enzyme';
import chai from 'chai';
import Form from './Form';

const expect = chai.expect;

const debug = (component) => {
    console.log('------------------------------------------------------------');
    console.log(component.debug());
    console.log('------------------------------------------------------------');
};

/* *****************************************************************************
the Form component
    should be a <form> element
*/
describe('the Form component', () => {

    it('should be a <form> element', () => {
        const component = shallow(<Form />);

        expect(component.is('form')).to.equal(true);
    });

});

/* *****************************************************************************
a Form component with a TextInput element
    should include a <Form.TextInput> as a child
*/
describe('a Form component with a TextInput element', () => {

    it('should include a <Form.TextInput> as a child', () => {
        const component = shallow(
            <Form>
                <Form.TextInput />
            </Form>
        );

        expect(component.find(Form.TextInput)).to.have.length(1);
    });
});

/* *****************************************************************************
when initializing a Form with a required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when initializing a Form with a required TextInput', () => {

    const required = true;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when initializing a Form with a non-required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when initializing a Form with a non-required TextInput', () => {

    const required = false;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = '';

        const component = render(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing the value of a required TextInput (but not blurring)
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when changing the value of a required TextInput (but not blurring)', () => {

    const required = true;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing the value of a non-required TextInput (but not blurring)
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message should not be displayed with an invalid value
    the component validation message should not be displayed with an invalid value
*/
describe('when changing the value of a non-required TextInput (but not blurring)', () => {

    const required = false;

    it('the validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'something else';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

});

/* *****************************************************************************
when changing (and blurring) the value of a required TextInput
    the global validation message should not be displayed with a valid value
    the component validation message should not be displayed with a valid value
    the global validation message SHOULD be displayed with an invalid value
    the component validation message SHOULD be displayed with an invalid value
*/
describe('when changing (and blurring) the value of a required TextInput', () => {

    const required = true;

    it('the global validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'valid';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(0);
    });

    it('the component validation message should not be displayed with a valid value', () => {
        const initialValue = 'something';
        const finalValue = 'valid';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('.has-error')).to.have.length(0);
        expect(component.find('.help-block')).to.have.length(0);
    });

    it('the global validation message SHOULD be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        expect(component.find('Alert')).to.have.length(1, 'Alert');
    });

    it('the component validation message SHOULD be displayed with an invalid value', () => {
        const initialValue = 'something';
        const finalValue = '';

        const component = mount(
            <Form>
                <Form.TextInput required={required} value={initialValue} />
            </Form>
        );

        component.find('input').simulate('change', {
            target: { value: finalValue }
        });

        component.find('input').simulate('blur', {
            target: { value: finalValue }
        });

        // debug(component);
        expect(component.find('.has-error')).to.have.length(1, 'has-error');
        expect(component.find('.help-block')).to.have.length(1, 'help-block');
    });
});

/* *****************************************************************************
when changing (and blurring) the value of a required TextInput with an invalid value
    the error message displayed in the form should be the default value when validationMessage is not set
    the error message displayed in the form should be the custom value when validationMessage is set
    the error message displayed in the component should be the default value when validationMessage is not set
    the error message displayed in the component should be the custom value when validationMessage is set
*/
describe('when changing (and blurring) the value of a required TextInput with an invalid value', () => {

    // it('the error message displayed in the form should be the default value when validationMessage is not set', () => {});

    // it('the error message displayed in the form should be the custom value when validationMessage is set', () => {});

    // it('the error message displayed in the component should be the default value when validationMessage is not set', () => {});

    // it('the error message displayed in the component should be the custom value when validationMessage is set', () => {});

});
<|endoftext|>


<commit_before>import React from 'react'
import {
  Col,
  ControlLabel,
  FormControl,
  FormGroup,
  Row
} from 'react-bootstrap'

import routes from '../routes.json'
import logo from '../images/ripta_logo.png'

export default class NextBuses extends React.Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleRouteChange = this.handleRouteChange.bind(this)
    this.state = {
      routeNumber: "1",
      stopId: "17045",
      stops: [],
      tripUpdates: []
    }
  }

  componentDidMount() {
    fetch(`http://localhost:8000/api/tripupdates/route/${this.state.routeNumber}`)
      .then(response => response.json())
      .then(json => {
        this.setState({ tripUpdates: json.entity })
      })

    fetch(`http://localhost:8000/api/route/${this.state.routeNumber}/stops`)
      .then(response => response.json())
      .then(json => {
        this.setState({ stops: json })
      })

  }

  handleChange(evt) {
    this.setState({ stopId: evt.target.value.toString()})
  }

  handleRouteChange(evt) {
    this.setState({ routeNumber: evt.target.value.toString()})
  }

  getStopTimeUpdates() {
    const buses = this.state.tripUpdates.map((tripUpdate) => {
      const stopUpdate = tripUpdate.trip_update.stop_time_update
        .find(item => item.stop_id === this.state.stopId)
      return stopUpdate
    })
    return buses
  }

  getStopName(id) {
    const stop = this.state.stops.find(
      stop => stop.stop_id === this.state.stopId)
    return stop ? stop.stop_name : ''
  }

  render() {
    const stopTimeUpdates = this.getStopTimeUpdates()
    const stopName = this.getStopName(this.state.stopId)

    return (

        <Row>
          <Col xs={12}>
            <FormGroup controlId="routes">
              <ControlLabel>Select a route</ControlLabel>
              <FormControl componentClass="select" placeholder="select"
                onChange={this.handleRouteChange}>
                <option value="">Select a route</option>
                {routes
                  .map(route => (
                  <option value={route.route_id}>{route.route_long_name}</option>
                ))}
              </FormControl>
            </FormGroup>

            <FormGroup controlId="stops">
              <ControlLabel>Select a stop</ControlLabel>
              <FormControl componentClass="select" placeholder="select"
                onChange={this.handleChange}>
                <option value="">Select a stop</option>
                {this.state.stops
                  .sort((a,b) => a.stop_name  (
                  <option value={stop.stop_id}>{stop.stop_name}</option>
                ))}
              </FormControl>
            </FormGroup>
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            {this.state.stopId + ': ' + stopName}

              {stopTimeUpdates
                .filter(update => update)
                .map(update => (

Arrives at: {new Date(update.arrival.time).toString()}
                    {' '}Delayed: {(update.arrival.delay/60)} mins

                )
                )}

          </Col>
        </Row>

    )
  }
}
<commit_msg>Added time to next bus<commit_after>import React from 'react'
import {
  Col,
  ControlLabel,
  FormControl,
  FormGroup,
  Row
} from 'react-bootstrap'

import routes from '../routes.json'
import logo from '../images/ripta_logo.png'

export default class NextBuses extends React.Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleRouteChange = this.handleRouteChange.bind(this)
    this.state = {
      routeNumber: "1",
      stopId: "17045",
      stops: [],
      tripUpdates: []
    }
  }

  componentDidMount() {
    fetch(`http://localhost:8000/api/tripupdates/route/${this.state.routeNumber}`)
      .then(response => response.json())
      .then(json => {
        this.setState({ tripUpdates: json.entity })
      })

    fetch(`http://localhost:8000/api/route/${this.state.routeNumber}/stops`)
      .then(response => response.json())
      .then(json => {
        this.setState({ stops: json })
      })

  }

  handleChange(evt) {
    this.setState({ stopId: evt.target.value.toString()})
  }

  handleRouteChange(evt) {
    this.setState({ routeNumber: evt.target.value.toString()})
  }

  getStopTimeUpdates() {
    const buses = this.state.tripUpdates.map((tripUpdate) => {
      const stopUpdate = tripUpdate.trip_update.stop_time_update
        .find(item => item.stop_id === this.state.stopId)
      return stopUpdate
    })
    return buses
  }

  getStopName(id) {
    const stop = this.state.stops.find(
      stop => stop.stop_id === this.state.stopId)
    return stop ? stop.stop_name : ''
  }

  render() {
    const stopTimeUpdates = this.getStopTimeUpdates()
    const stopName = this.getStopName(this.state.stopId)

//    var displayYear = new update.arrival.time.substr(0,3)
//    var displayMonth = new update.arrival.time.substr(4,5)

    return (

        <Row>
          <Col xs={12}>
            <FormGroup controlId="routes">
              <ControlLabel>Select a route</ControlLabel>
              <FormControl componentClass="select" placeholder="select"
                onChange={this.handleRouteChange}>
                <option value="">Select a route</option>
                {routes
                  .map(route => (
                  <option value={route.route_id}>{route.route_long_name}</option>
                ))}
              </FormControl>
            </FormGroup>

            <FormGroup controlId="stops">
              <ControlLabel>Select a stop</ControlLabel>
              <FormControl componentClass="select" placeholder="select"
                onChange={this.handleChange}>
                <option value="">Select a stop</option>
                {this.state.stops
                  .sort((a,b) => a.stop_name  (
                  <option value={stop.stop_id}>{stop.stop_name}</option>
                ))}
              </FormControl>
            </FormGroup>
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            {this.state.stopId + ': ' + stopName}

              {stopTimeUpdates
                .filter(update => update)
                .map(update => (

Next bus is in: {(update.arrival.time - new Date()
                        .getTime()/1000)/60} minutes

This bus will arrive at:
                        {new Date(update.arrival.time * 1000).toString()}

This bus is delayed by:
                        {(update.arrival.delay/60)} minutes

                )
                )}

          </Col>
        </Row>

    )
  }
}
<|endoftext|>


<commit_before>import _ from 'lodash'
import React from 'react/addons'
import getEventKey from 'react/lib/getEventKey'
import Mousetrap from 'mousetrap'

import EditorActions from '../flux/EditorActions'
import parseHtml from '../core/htmlparser'
import { emptyNode } from '../core/dom'

const T = React.PropTypes
const MIME_TYPE_TEXT_PLAIN = 'text/plain'
const MIME_TYPE_TEXT_HTML = 'text/html'
const MIME_TYPE_RITZY_RICH_TEXT = 'application/x-ritzy-rt'

/**
 * A React component to obtain TextInput from the user and insert it into the editor. We capture shortcuts
 * via Mousetrap.js bindings. All other characters bypass Mousetrap and are inserted by the browser into the
 * focused hidden text input area (so the browser's internal machinery deals with the key capture and translation
 * to characters, so it should work for complex IME input as well), and from there the browser triggers the
 * onInput event on the input area, at which point we grab the character and place it into the editor.
 */
export default React.createClass({
  propTypes: {
    id: T.string.isRequired,
    yPosition: T.number.isRequired,
    focused: T.bool.isRequired,
    renderOptimizations: T.bool
  },

  getDefaultProps() {
    return {
      renderOptimizations: true
    }
  },

  componentDidMount() {
    this.input = React.findDOMNode(this.refs.input)
    this.hiddenContainer = React.findDOMNode(this.refs.hiddenContainer)
    this.ieClipboardDiv = React.findDOMNode(this.refs.ieClipboardDiv)

    // for HTML paste support in IE -- ignored by Chrome/Firefox
    this.input.addEventListener('beforepaste', () => {
      if (document.activeElement !== this.ieClipboardDiv) {
        this._focusIeClipboardDiv()
      }
    }, true)

    let keyBindings = new Mousetrap(this.input)
    keyBindings.bind(['up', 'down', 'left', 'right'], this._handleKeyArrow)
    keyBindings.bind(['shift+up', 'shift+down', 'shift+left', 'shift+right'], this._handleKeySelectionArrow)
    keyBindings.bind(['pageup', 'pagedown'], this._handleKeyNavigationPage)
    keyBindings.bind(['shift+pageup', 'shift+pagedown'], this._handleKeySelectionPage)
    keyBindings.bind(['mod+home', 'home', 'mod+end', 'end'], this._handleKeyNavigationHomeEnd)
    keyBindings.bind(['mod+shift+home', 'shift+home', 'mod+shift+end', 'shift+end'], this._handleKeySelectionHomeEnd)
    keyBindings.bind(['mod+left', 'mod+right'], this._handleKeyNavigationWord)
    keyBindings.bind(['shift+mod+left', 'shift+mod+right'], this._handleKeySelectionWord)
    keyBindings.bind('mod+a', this._handleKeySelectionAll)
    keyBindings.bind('backspace', this._handleKeyBackspace)
    keyBindings.bind('del', this._handleKeyDelete)
    keyBindings.bind('mod+backspace', this._handleKeyWordBackspace)
    keyBindings.bind('mod+del', this._handleKeyWordDelete)
    //keyBindings.bind('mod+s', this._handleKeySave)
    //keyBindings.bind('tab', this._handleKeyTab)
    //keyBindings.bind('mod+z', this._handleUndo)
    //keyBindings.bind('mod+y', this._handleRedo)

    keyBindings.bind('mod+b', this._handleKeyBold)
    keyBindings.bind('mod+i', this._handleKeyItalics)
    keyBindings.bind('mod+u', this._handleKeyUnderline)
    keyBindings.bind('alt+shift+5', this._handleKeyStrikethrough)
    keyBindings.bind('mod+.', this._handleKeySuperscript)
    keyBindings.bind('mod+,', this._handleKeySubscript)

    if(this.props.focused) {
      this._focus()
    }
  },

  shouldComponentUpdate(nextProps) {
    if(!nextProps.renderOptimizations) {
      return true
    }

    return this.props.focused !== nextProps.focused || this.props.yPosition !== nextProps.yPosition
  },

  componentDidUpdate() {
    if(this.props.focused) {
      this._focus()
    }
  },

  _focus() {
    this._checkEmptyValue()

    this.ieClipboardDivFocus = false
    this.focusHappening = true
    try {
      // IE requires a non-empty selection in order to fire the copy event, annoying
      this.input.value = ' '
      this.input.focus()
      this.input.select()
    } finally {
      this.focusHappening = false
    }
  },

  _checkEmptyValue() {
    let value = this.input.value
    if(value.length > 0 && value !== ' ') {
      console.warn('The hidden input area is not empty, missed an event? Inserting now. Please report this to https://github.com/ritzyed/ritzy/issues/12. Value=', value)
      EditorActions.insertChars(value)
      this.input.value = ' '
    }
  },

  _handleKeyArrow(e, key) {
    this._checkEmptyValue()

    if(key === 'left') {
      EditorActions.navigateLeft()
    } else if(key === 'right') {
      EditorActions.navigateRight()
    } else if(key === 'up') {
      EditorActions.navigateUp()
    } else if(key === 'down') {
      EditorActions.navigateDown()
    }
    return false
  },

  _handleKeySelectionArrow(e, key) {
    this._checkEmptyValue()

    if(key === 'shift+left') {
      EditorActions.selectionLeft()
    } else if(key === 'shift+right') {
      EditorActions.selectionRight()
    } else if(key === 'shift+up') {
      EditorActions.selectionUp()
    } else if(key === 'shift+down') {
      EditorActions.selectionDown()
    }
    return false
  },

  _handleKeyBackspace() {
    this._checkEmptyValue()

    EditorActions.eraseCharBack()
    return false
  },

  _handleKeyDelete() {
    this._checkEmptyValue()

    EditorActions.eraseCharForward()
    return false
  },

  _handleKeyWordBackspace() {
    this._checkEmptyValue()

    EditorActions.eraseWordBack()
    return false
  },

  _handleKeyWordDelete() {
    this._checkEmptyValue()

    EditorActions.eraseWordForward()
    return false
  },

  _handleKeyNavigationPage(e, key) {
    if(key === 'pageup') {
      EditorActions.navigatePageUp()
    } else if(key === 'pagedown') {
      EditorActions.navigatePageDown()
    }
    return false
  },

  _handleKeySelectionPage(e, key) {
    if(key === 'shift+pageup') {
      EditorActions.selectionPageUp()
    } else if(key === 'shift+pagedown') {
      EditorActions.selectionPageDown()
    }
    return false
  },

  _handleKeyNavigationHomeEnd(e, key) {
    if(key === 'mod+home') {
      EditorActions.navigateStart()
    } else if(key === 'home') {
      EditorActions.navigateStartLine()
    } else if(key === 'mod+end') {
      EditorActions.navigateEnd()
    } else if(key === 'end') {
      EditorActions.navigateEndLine()
    }
    return false
  },

  _handleKeyNavigationWord(e, key) {
    this._checkEmptyValue()

    if(key === 'mod+left') {
      EditorActions.navigateWordLeft()
    } else if(key === 'mod+right') {
      EditorActions.navigateWordRight()
    }
    return false
  },

  _handleKeySelectionHomeEnd(e, key) {
    if(key === 'mod+shift+home') {
      EditorActions.selectionStart()
    } else if(key === 'shift+home') {
      EditorActions.selectionStartLine()
    } else if(key === 'mod+shift+end') {
      EditorActions.selectionEnd()
    } else if(key === 'shift+end') {
      EditorActions.selectionEndLine()
    }

    return false
  },

  _handleKeySelectionWord(e, key) {
    if(key === 'shift+mod+left') {
      EditorActions.selectionWordLeft()
    } else if(key === 'shift+mod+right') {
      EditorActions.selectionWordRight()
    }
    return false
  },

  _handleKeySelectionAll() {
    EditorActions.selectionAll()
    return false
  },

  _handleKeyBold() {
    EditorActions.toggleBold()
    return false
  },

  _handleKeyItalics() {
    EditorActions.toggleItalics()
    return false
  },

  _handleKeyUnderline() {
    EditorActions.toggleUnderline()
    return false
  },

  _handleKeyStrikethrough() {
    EditorActions.toggleStrikethrough()
    return false
  },

  _handleKeySuperscript() {
    EditorActions.toggleSuperscript()
    return false
  },

  _handleKeySubscript() {
    EditorActions.toggleSubscript()
    return false
  },

  _onInputFocusLost() {
    if(!this.ieClipboardDivFocus) {
      EditorActions.inputFocusLost()
    }
  },

  _onInputFocusGained() {
    if(!this.focusHappening && !this.ieClipboardDivFocus && this.input === document.activeElement) {
      // regaining focus, perhaps the user switched windows and the browser is now focused again
      EditorActions.focusInput()
    }
  },

  _onCopy(e) {
    let copyHandler = window.clipboardData ?
      _.partial(this._handleIeCopy) :
      _.partial(this._handleNormalCopy, e)

    EditorActions.copySelection(copyHandler)
  },

  _onCut(e) {
    this._onCopy(e)
    EditorActions.eraseSelection()
  },

  _handleNormalCopy(e, copiedText, copiedHtml, copiedRich) {
    e.clipboardData.setData(MIME_TYPE_TEXT_PLAIN, copiedText)
    e.clipboardData.setData(MIME_TYPE_TEXT_HTML, copiedHtml)
    // some browsers e.g. Chrome support arbitrary MIME types, makes paste way more efficient
    // Firefox allows setting the type, but not pasting it -- see https://bugzilla.mozilla.org/show_bug.cgi?id=860857
    e.clipboardData.setData(MIME_TYPE_RITZY_RICH_TEXT, JSON.stringify(copiedRich))
    e.preventDefault()
    e.stopPropagation()
  },

  _handleIeCopy(copiedText, copiedHtml) {
    window.clipboardData.setData('Text', copiedText)
    this.ieClipboardDiv.innerHTML = copiedHtml
    this._focusIeClipboardDiv()
    this._selectIeClipboardDivContents()
    setTimeout(() => {
      emptyNode(this.ieClipboardDiv)
      this._focus()
    }, 0)
  },

  _onPaste(e) {
    this._checkEmptyValue()
    if(e.clipboardData.types) {
      this._handleNormalPaste(e)
    } else {
      // IE does not provide access to HTML in the paste event but does allow HTML to paste into a contentEditable
      // see http://stackoverflow.com/a/27279218/430128
      this._handleIePaste()
    }
  },

  _handleNormalPaste(e) {
    let clipboardDataTypes
    if(e.clipboardData.types.findIndex) {
      clipboardDataTypes = e.clipboardData.types
    } else {
      // Firefox uses DOMStringList instead of an array type, convert it
      clipboardDataTypes = Array.from(e.clipboardData.types)
    }

    if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_RITZY_RICH_TEXT) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_RITZY_RICH_TEXT)
      EditorActions.insertCharsBatch(JSON.parse(pasted))
    } else if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_TEXT_HTML) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_TEXT_HTML)
      let pastedChunks = parseHtml(pasted, this.hiddenContainer)
      EditorActions.insertCharsBatch(pastedChunks)
    } else if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_TEXT_PLAIN) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_TEXT_PLAIN)
      EditorActions.insertChars(pasted)
    } else {
      console.warn('Paste not supported yet for types: ' + JSON.stringify(clipboardDataTypes))
    }
    e.preventDefault()
    e.stopPropagation()
  },

  _handleIePaste() {
    setTimeout(() => {
      let pasted = this.ieClipboardDiv.innerHTML
      try {
        let pastedChunks = parseHtml(pasted, this.hiddenContainer)
        EditorActions.insertCharsBatch(pastedChunks)
      } finally {
        emptyNode(this.ieClipboardDiv)
        this._focus()
      }
    }, 0)
  },

  _focusIeClipboardDiv() {
    this.ieClipboardDivFocus = true
    this.ieClipboardDiv.focus()
  },

  _selectIeClipboardDivContents() {
    let range = document.createRange()
    range.selectNodeContents(this.ieClipboardDiv)
    let selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange(range)
  },

  _onInput(e) {
    // all character input, including IME events should trigger this event
    let value = e.target.value
    EditorActions.insertChars(value)
    e.target.value = ' '
    this._focus()
  },

  render() {
    let divStyle = {
      position: 'absolute',
      overflow: 'hidden',
      height: 0,
      outline: 'none',
      left: 0,
      top: this.props.yPosition
    }

    // we can't focus an element with display: none so wrap them in another invisible div
    return (

        <textarea key="input" ref="input" onInput={this._onInput}
          onCopy={this._onCopy} onCut={this._onCut} onPaste={this._onPaste} onBlur={this._onInputFocusLost} onFocus={this._onInputFocusGained}/>

    )
  }

})
<commit_msg>Use React onChange instead of onInput in TextInput<commit_after>import _ from 'lodash'
import React from 'react/addons'
import getEventKey from 'react/lib/getEventKey'
import Mousetrap from 'mousetrap'

import EditorActions from '../flux/EditorActions'
import parseHtml from '../core/htmlparser'
import { emptyNode } from '../core/dom'

const T = React.PropTypes
const MIME_TYPE_TEXT_PLAIN = 'text/plain'
const MIME_TYPE_TEXT_HTML = 'text/html'
const MIME_TYPE_RITZY_RICH_TEXT = 'application/x-ritzy-rt'

/**
 * A React component to obtain TextInput from the user and insert it into the editor. We capture shortcuts
 * via Mousetrap.js bindings. All other characters bypass Mousetrap and are inserted by the browser into the
 * focused hidden text input area (so the browser's internal machinery deals with the key capture and translation
 * to characters, so it should work for complex IME input as well), and from there the browser triggers the
 * onInput event on the input area, at which point we grab the character and place it into the editor.
 */
export default React.createClass({
  propTypes: {
    id: T.string.isRequired,
    yPosition: T.number.isRequired,
    focused: T.bool.isRequired,
    renderOptimizations: T.bool
  },

  getDefaultProps() {
    return {
      renderOptimizations: true
    }
  },

  componentDidMount() {
    this.input = React.findDOMNode(this.refs.input)
    this.hiddenContainer = React.findDOMNode(this.refs.hiddenContainer)
    this.ieClipboardDiv = React.findDOMNode(this.refs.ieClipboardDiv)

    // for HTML paste support in IE -- ignored by Chrome/Firefox
    this.input.addEventListener('beforepaste', () => {
      if (document.activeElement !== this.ieClipboardDiv) {
        this._focusIeClipboardDiv()
      }
    }, true)

    let keyBindings = new Mousetrap(this.input)
    keyBindings.bind(['up', 'down', 'left', 'right'], this._handleKeyArrow)
    keyBindings.bind(['shift+up', 'shift+down', 'shift+left', 'shift+right'], this._handleKeySelectionArrow)
    keyBindings.bind(['pageup', 'pagedown'], this._handleKeyNavigationPage)
    keyBindings.bind(['shift+pageup', 'shift+pagedown'], this._handleKeySelectionPage)
    keyBindings.bind(['mod+home', 'home', 'mod+end', 'end'], this._handleKeyNavigationHomeEnd)
    keyBindings.bind(['mod+shift+home', 'shift+home', 'mod+shift+end', 'shift+end'], this._handleKeySelectionHomeEnd)
    keyBindings.bind(['mod+left', 'mod+right'], this._handleKeyNavigationWord)
    keyBindings.bind(['shift+mod+left', 'shift+mod+right'], this._handleKeySelectionWord)
    keyBindings.bind('mod+a', this._handleKeySelectionAll)
    keyBindings.bind('backspace', this._handleKeyBackspace)
    keyBindings.bind('del', this._handleKeyDelete)
    keyBindings.bind('mod+backspace', this._handleKeyWordBackspace)
    keyBindings.bind('mod+del', this._handleKeyWordDelete)
    //keyBindings.bind('mod+s', this._handleKeySave)
    //keyBindings.bind('tab', this._handleKeyTab)
    //keyBindings.bind('mod+z', this._handleUndo)
    //keyBindings.bind('mod+y', this._handleRedo)

    keyBindings.bind('mod+b', this._handleKeyBold)
    keyBindings.bind('mod+i', this._handleKeyItalics)
    keyBindings.bind('mod+u', this._handleKeyUnderline)
    keyBindings.bind('alt+shift+5', this._handleKeyStrikethrough)
    keyBindings.bind('mod+.', this._handleKeySuperscript)
    keyBindings.bind('mod+,', this._handleKeySubscript)

    if(this.props.focused) {
      this._focus()
    }
  },

  shouldComponentUpdate(nextProps) {
    if(!nextProps.renderOptimizations) {
      return true
    }

    return this.props.focused !== nextProps.focused || this.props.yPosition !== nextProps.yPosition
  },

  componentDidUpdate() {
    if(this.props.focused) {
      this._focus()
    }
  },

  _focus() {
    this._checkEmptyValue()

    this.ieClipboardDivFocus = false
    this.focusHappening = true
    try {
      // IE requires a non-empty selection in order to fire the copy event, annoying
      this.input.value = ' '
      this.input.focus()
      this.input.select()
    } finally {
      this.focusHappening = false
    }
  },

  _checkEmptyValue() {
    let value = this.input.value
    if(value.length > 0 && value !== ' ') {
      console.warn('The hidden input area is not empty, missed an event? Inserting now. Please report this to https://github.com/ritzyed/ritzy/issues/12. Value=', value)
      EditorActions.insertChars(value)
      this.input.value = ' '
    }
  },

  _handleKeyArrow(e, key) {
    this._checkEmptyValue()

    if(key === 'left') {
      EditorActions.navigateLeft()
    } else if(key === 'right') {
      EditorActions.navigateRight()
    } else if(key === 'up') {
      EditorActions.navigateUp()
    } else if(key === 'down') {
      EditorActions.navigateDown()
    }
    return false
  },

  _handleKeySelectionArrow(e, key) {
    this._checkEmptyValue()

    if(key === 'shift+left') {
      EditorActions.selectionLeft()
    } else if(key === 'shift+right') {
      EditorActions.selectionRight()
    } else if(key === 'shift+up') {
      EditorActions.selectionUp()
    } else if(key === 'shift+down') {
      EditorActions.selectionDown()
    }
    return false
  },

  _handleKeyBackspace() {
    this._checkEmptyValue()

    EditorActions.eraseCharBack()
    return false
  },

  _handleKeyDelete() {
    this._checkEmptyValue()

    EditorActions.eraseCharForward()
    return false
  },

  _handleKeyWordBackspace() {
    this._checkEmptyValue()

    EditorActions.eraseWordBack()
    return false
  },

  _handleKeyWordDelete() {
    this._checkEmptyValue()

    EditorActions.eraseWordForward()
    return false
  },

  _handleKeyNavigationPage(e, key) {
    if(key === 'pageup') {
      EditorActions.navigatePageUp()
    } else if(key === 'pagedown') {
      EditorActions.navigatePageDown()
    }
    return false
  },

  _handleKeySelectionPage(e, key) {
    if(key === 'shift+pageup') {
      EditorActions.selectionPageUp()
    } else if(key === 'shift+pagedown') {
      EditorActions.selectionPageDown()
    }
    return false
  },

  _handleKeyNavigationHomeEnd(e, key) {
    if(key === 'mod+home') {
      EditorActions.navigateStart()
    } else if(key === 'home') {
      EditorActions.navigateStartLine()
    } else if(key === 'mod+end') {
      EditorActions.navigateEnd()
    } else if(key === 'end') {
      EditorActions.navigateEndLine()
    }
    return false
  },

  _handleKeyNavigationWord(e, key) {
    this._checkEmptyValue()

    if(key === 'mod+left') {
      EditorActions.navigateWordLeft()
    } else if(key === 'mod+right') {
      EditorActions.navigateWordRight()
    }
    return false
  },

  _handleKeySelectionHomeEnd(e, key) {
    if(key === 'mod+shift+home') {
      EditorActions.selectionStart()
    } else if(key === 'shift+home') {
      EditorActions.selectionStartLine()
    } else if(key === 'mod+shift+end') {
      EditorActions.selectionEnd()
    } else if(key === 'shift+end') {
      EditorActions.selectionEndLine()
    }

    return false
  },

  _handleKeySelectionWord(e, key) {
    if(key === 'shift+mod+left') {
      EditorActions.selectionWordLeft()
    } else if(key === 'shift+mod+right') {
      EditorActions.selectionWordRight()
    }
    return false
  },

  _handleKeySelectionAll() {
    EditorActions.selectionAll()
    return false
  },

  _handleKeyBold() {
    EditorActions.toggleBold()
    return false
  },

  _handleKeyItalics() {
    EditorActions.toggleItalics()
    return false
  },

  _handleKeyUnderline() {
    EditorActions.toggleUnderline()
    return false
  },

  _handleKeyStrikethrough() {
    EditorActions.toggleStrikethrough()
    return false
  },

  _handleKeySuperscript() {
    EditorActions.toggleSuperscript()
    return false
  },

  _handleKeySubscript() {
    EditorActions.toggleSubscript()
    return false
  },

  _onInputFocusLost() {
    if(!this.ieClipboardDivFocus) {
      EditorActions.inputFocusLost()
    }
  },

  _onInputFocusGained() {
    if(!this.focusHappening && !this.ieClipboardDivFocus && this.input === document.activeElement) {
      // regaining focus, perhaps the user switched windows and the browser is now focused again
      EditorActions.focusInput()
    }
  },

  _onCopy(e) {
    let copyHandler = window.clipboardData ?
      _.partial(this._handleIeCopy) :
      _.partial(this._handleNormalCopy, e)

    EditorActions.copySelection(copyHandler)
  },

  _onCut(e) {
    this._onCopy(e)
    EditorActions.eraseSelection()
  },

  _handleNormalCopy(e, copiedText, copiedHtml, copiedRich) {
    e.clipboardData.setData(MIME_TYPE_TEXT_PLAIN, copiedText)
    e.clipboardData.setData(MIME_TYPE_TEXT_HTML, copiedHtml)
    // some browsers e.g. Chrome support arbitrary MIME types, makes paste way more efficient
    // Firefox allows setting the type, but not pasting it -- see https://bugzilla.mozilla.org/show_bug.cgi?id=860857
    e.clipboardData.setData(MIME_TYPE_RITZY_RICH_TEXT, JSON.stringify(copiedRich))
    e.preventDefault()
    e.stopPropagation()
  },

  _handleIeCopy(copiedText, copiedHtml) {
    window.clipboardData.setData('Text', copiedText)
    this.ieClipboardDiv.innerHTML = copiedHtml
    this._focusIeClipboardDiv()
    this._selectIeClipboardDivContents()
    setTimeout(() => {
      emptyNode(this.ieClipboardDiv)
      this._focus()
    }, 0)
  },

  _onPaste(e) {
    this._checkEmptyValue()
    if(e.clipboardData.types) {
      this._handleNormalPaste(e)
    } else {
      // IE does not provide access to HTML in the paste event but does allow HTML to paste into a contentEditable
      // see http://stackoverflow.com/a/27279218/430128
      this._handleIePaste()
    }
  },

  _handleNormalPaste(e) {
    let clipboardDataTypes
    if(e.clipboardData.types.findIndex) {
      clipboardDataTypes = e.clipboardData.types
    } else {
      // Firefox uses DOMStringList instead of an array type, convert it
      clipboardDataTypes = Array.from(e.clipboardData.types)
    }

    if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_RITZY_RICH_TEXT) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_RITZY_RICH_TEXT)
      EditorActions.insertCharsBatch(JSON.parse(pasted))
    } else if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_TEXT_HTML) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_TEXT_HTML)
      let pastedChunks = parseHtml(pasted, this.hiddenContainer)
      EditorActions.insertCharsBatch(pastedChunks)
    } else if(clipboardDataTypes.findIndex(t => t === MIME_TYPE_TEXT_PLAIN) > -1) {
      let pasted = e.clipboardData.getData(MIME_TYPE_TEXT_PLAIN)
      EditorActions.insertChars(pasted)
    } else {
      console.warn('Paste not supported yet for types: ' + JSON.stringify(clipboardDataTypes))
    }
    e.preventDefault()
    e.stopPropagation()
  },

  _handleIePaste() {
    setTimeout(() => {
      let pasted = this.ieClipboardDiv.innerHTML
      try {
        let pastedChunks = parseHtml(pasted, this.hiddenContainer)
        EditorActions.insertCharsBatch(pastedChunks)
      } finally {
        emptyNode(this.ieClipboardDiv)
        this._focus()
      }
    }, 0)
  },

  _focusIeClipboardDiv() {
    this.ieClipboardDivFocus = true
    this.ieClipboardDiv.focus()
  },

  _selectIeClipboardDivContents() {
    let range = document.createRange()
    range.selectNodeContents(this.ieClipboardDiv)
    let selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange(range)
  },

  _onChange(e) {
    // all character input, including IME events should trigger this event
    let value = e.target.value
    EditorActions.insertChars(value)
    e.target.value = ' '
    this._focus()
  },

  render() {
    let divStyle = {
      position: 'absolute',
      overflow: 'hidden',
      height: 0,
      outline: 'none',
      left: 0,
      top: this.props.yPosition
    }

    // we can't focus an element with display: none so wrap them in another invisible div
    return (

        <textarea key="input" ref="input" onChange={this._onChange}
          onCopy={this._onCopy} onCut={this._onCut} onPaste={this._onPaste} onBlur={this._onInputFocusLost} onFocus={this._onInputFocusGained}/>

    )
  }

})
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import IntroScreen from './IntroScreen';
import GameBoard from './GameBoard';
import ScoreBoard from './ScoreBoard';
import InfoDisplay from './InfoDisplay';
import rowLogic, { winningLines } from './functions/rowLogic';
import forkLogic from './functions/forkLogic';
import { cleanBoard } from '../reducers/gameBoard';
import * as actions from '../actions/index';
import * as show from '../constants/infoDisplayConstants';

export class TicTacToe extends Component {
  constructor (props) {
    super(props);

    this.playerHasChosen = this.playerHasChosen.bind(this);
    this.newPlayerMove = this.newPlayerMove.bind(this);
    this.crownWinner = this.crownWinner.bind(this);
    this.restartGame = this.restartGame.bind(this);
    this.computerMove = this.computerMove.bind(this);
    this.whosMove = this.whosMove.bind(this);
    this.whoStarts = this.whoStarts.bind(this);
  }

  playerHasChosen (playerToken, computerToken) {
    this.props.showIntroScreen(false);
    this.props.setTokens(playerToken, computerToken);
  }

  newPlayerMove (position) {
    let currentBoard = this.props.gameBoard.slice();
    currentBoard[position] = this.props.tokens.playerToken;
    this.props.updateTheBoard(currentBoard);
    this.checkIfWinner(currentBoard);
  }

  checkIfWinner (Board) {
    let gameOver = false;
    winningLines.forEach((winLine) => {
      let [winIdxOne, winIdxTwo, winIdxThree] = winLine;
      if (Board[winIdxOne] + Board[winIdxTwo] + Board[winIdxThree] === 'XXX' ||
          Board[winIdxOne] + Board[winIdxTwo] + Board[winIdxThree] === 'OOO') {
        gameOver = true;
        return this.crownWinner(...winLine);
      }
    });
    if (Board.join('').length === 9) {
      return this.tieGame();
    }
    if (!gameOver) {
      return this.whosMove();
    }
  }

  tieGame () {
    this.props.changeInfoDisplay(show.TIE_GAME);
    setTimeout(this.restartGame, 3000);
  }

  whosMove () {
    const {
      updateTurnNumber,
      turnNumber,
      playersTurn,
      changeWhosTurn,
      changeInfoDisplay,
    } = this.props;

    updateTurnNumber(turnNumber + 1);
    if (playersTurn) {
      changeWhosTurn(false);
      changeInfoDisplay(show.THINKING);
      setTimeout(this.computerMove, 1000);
    } else {
      changeWhosTurn(true);
      changeInfoDisplay(show.YOUR_TURN);
    }
  }

  whoStarts () {
    const { playerStarts, changeWhosTurn, whoStartsNext, changeInfoDisplay } = this.props;

    if (playerStarts) {
      changeWhosTurn(false);
      whoStartsNext();
      changeInfoDisplay(show.THINKING);
      setTimeout(this.computerMove, 1000);
    } else {
      changeWhosTurn(true);
      whoStartsNext();
      changeInfoDisplay(show.YOUR_TURN);
    }
  }

  crownWinner (winIdxOne, winIdxTwo, winIdxThree) {
    const { boxColors, changeBoxColors, changeInfoDisplay, addToComputerScore } = this.props;

    const winningColor = '#EFD469';
    let Colors = JSON.parse(JSON.stringify(boxColors));
    Colors[winIdxOne].backgroundColor = winningColor;
    Colors[winIdxTwo].backgroundColor = winningColor;
    Colors[winIdxThree].backgroundColor = winningColor;
    changeBoxColors(Colors);
    changeInfoDisplay(show.YOU_LOST);
    addToComputerScore();
    setTimeout(this.restartGame, 3000);
  }

  restartGame () {
    const { boxColors, changeBoxColors, updateTheBoard, updateTurnNumber } = this.props;
    let boardCopy = _.cloneDeep(boxColors);
    let freshBoard = boardCopy.map((eachColor) => {
      eachColor.backgroundColor = '#D2D2D2';
      return eachColor;
    });
    changeBoxColors(freshBoard);
    updateTheBoard(cleanBoard);
    updateTurnNumber(1);
    this.whoStarts();
  }

  computerMove () {
    const { turnNumber, updateTheBoard } = this.props;
    const { playerToken, computerToken } = this.props.tokens;
    let gameBoard = this.props.gameBoard.slice();

    if (turnNumber === 1) {
      gameBoard[0] = computerToken;
    }
    if (turnNumber === 2) {
      if (gameBoard[4] === '') {
        gameBoard[4] = computerToken;
      } else gameBoard[2] = computerToken;
    }

    if (turnNumber === 3) {
      if (gameBoard[4] === '') {
        gameBoard[4] = computerToken;
      } else gameBoard[8] = computerToken;
    }

    if (turnNumber === 4) {
      gameBoard = rowLogic(gameBoard, playerToken, computerToken);

      if (gameBoard.join('').length === 3) {
        gameBoard = forkLogic(gameBoard, playerToken, computerToken);
      }
      if (gameBoard.join('').length === 3) {
        if (gameBoard[1] !== playerToken) {
          gameBoard[1] = computerToken;
        } else gameBoard[3] = computerToken;
      }
    }
    if (turnNumber > 4) {
      gameBoard = rowLogic(gameBoard, computerToken, computerToken);

      if (gameBoard.join('').length === turnNumber - 1) {
        gameBoard = rowLogic(gameBoard, playerToken, computerToken);
      }
      if (gameBoard.join('').length === turnNumber - 1) {
        let i = 0;
        while (gameBoard[i] !== '') i += 1;
        gameBoard[i] = computerToken;
      }
    }

    updateTheBoard(gameBoard);
    this.checkIfWinner(gameBoard);
  }

  render () {
    if (this.props.introScreen) {
      return (
        <IntroScreen
          className="IntroScreen"
          chooseThis={this.playerHasChosen}
        />
      );
    } else return (

        <InfoDisplay
          info={this.props.infoDisplay}
        />
        <GameBoard
          playersTurn={this.props.playersTurn}
          nextMove={this.newPlayerMove}
          squareContains={this.props.gameBoard}
          boxColors={this.props.boxColors}
        />
        <ScoreBoard
          compScore={this.props.computerScore}
        />

    );
  }
}

TicTacToe.propTypes = {

  /* action creators */

  changeInfoDisplay: PropTypes.func.isRequired,
  showIntroScreen: PropTypes.func.isRequired,
  addToComputerScore: PropTypes.func.isRequired,
  changeWhosTurn: PropTypes.func.isRequired,
  updateTurnNumber: PropTypes.func.isRequired,
  whoStartsNext: PropTypes.func.isRequired,
  setTokens: PropTypes.func.isRequired,
  changeBoxColors: PropTypes.func.isRequired,
  updateTheBoard: PropTypes.func.isRequired,

  /* properties */

  infoDisplay: PropTypes.string.isRequired,
  introScreen: PropTypes.bool.isRequired,
  computerScore: PropTypes.number.isRequired,
  playersTurn: PropTypes.bool.isRequired,
  turnNumber: PropTypes.number.isRequired,
  playerStarts: PropTypes.bool.isRequired,
  tokens: PropTypes.shape({
    playerToken: PropTypes.string.isRequired,
    computerToken: PropTypes.string.isRequired,
  }).isRequired,
  boxColors: PropTypes.arrayOf(PropTypes.shape({
    backgroundColor: PropTypes.string.isRequired,
  }).isRequired).isRequired,
  gameBoard: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
};

/* eslint-disable func-style */

function mapStateToProps ({
  infoDisplay,
  introScreen,
  computerScore,
  playersTurn,
  turnNumber,
  playerStarts,
  tokens,
  boxColors,
  gameBoard,
}) {
  return {
    infoDisplay,
    introScreen,
    computerScore,
    playersTurn,
    turnNumber,
    playerStarts,
    tokens,
    boxColors,
    gameBoard,
  };
}

function mapDispatchToProps (dispatch) {
  return bindActionCreators(Object.assign({}, actions), dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(TicTacToe);
<commit_msg>fixed destructuring<commit_after>import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import IntroScreen from './IntroScreen';
import GameBoard from './GameBoard';
import ScoreBoard from './ScoreBoard';
import InfoDisplay from './InfoDisplay';
import rowLogic, { winningLines } from './functions/rowLogic';
import forkLogic from './functions/forkLogic';
import { cleanBoard } from '../reducers/gameBoard';
import * as actions from '../actions/index';
import * as show from '../constants/infoDisplayConstants';

export class TicTacToe extends Component {
  constructor (props) {
    super(props);

    this.playerHasChosen = this.playerHasChosen.bind(this);
    this.newPlayerMove = this.newPlayerMove.bind(this);
    this.crownWinner = this.crownWinner.bind(this);
    this.restartGame = this.restartGame.bind(this);
    this.computerMove = this.computerMove.bind(this);
    this.whosMove = this.whosMove.bind(this);
    this.whoStarts = this.whoStarts.bind(this);
  }

  playerHasChosen (playerToken, computerToken) {
    this.props.showIntroScreen(false);
    this.props.setTokens(playerToken, computerToken);
  }

  newPlayerMove (position) {
    const { gameBoard, updateTheBoard, tokens: { playerToken } } = this.props;
    let currentBoard = gameBoard.slice();
    currentBoard[position] = playerToken;
    updateTheBoard(currentBoard);
    this.checkIfWinner(currentBoard);
  }

  checkIfWinner (Board) {
    let gameOver = false;
    winningLines.forEach((winLine) => {
      let [winIdxOne, winIdxTwo, winIdxThree] = winLine;
      if (Board[winIdxOne] + Board[winIdxTwo] + Board[winIdxThree] === 'XXX' ||
          Board[winIdxOne] + Board[winIdxTwo] + Board[winIdxThree] === 'OOO') {
        gameOver = true;
        return this.crownWinner(...winLine);
      }
    });
    if (Board.join('').length === 9) {
      return this.tieGame();
    }
    if (!gameOver) {
      return this.whosMove();
    }
  }

  tieGame () {
    this.props.changeInfoDisplay(show.TIE_GAME);
    setTimeout(this.restartGame, 3000);
  }

  whosMove () {
    const {
      updateTurnNumber,
      turnNumber,
      playersTurn,
      changeWhosTurn,
      changeInfoDisplay,
    } = this.props;

    updateTurnNumber(turnNumber + 1);
    if (playersTurn) {
      changeWhosTurn(false);
      changeInfoDisplay(show.THINKING);
      setTimeout(this.computerMove, 1000);
    } else {
      changeWhosTurn(true);
      changeInfoDisplay(show.YOUR_TURN);
    }
  }

  whoStarts () {
    const { playerStarts, changeWhosTurn, whoStartsNext, changeInfoDisplay } = this.props;

    if (playerStarts) {
      changeWhosTurn(false);
      whoStartsNext();
      changeInfoDisplay(show.THINKING);
      setTimeout(this.computerMove, 1000);
    } else {
      changeWhosTurn(true);
      whoStartsNext();
      changeInfoDisplay(show.YOUR_TURN);
    }
  }

  crownWinner (winIdxOne, winIdxTwo, winIdxThree) {
    const { boxColors, changeBoxColors, changeInfoDisplay, addToComputerScore } = this.props;

    const winningColor = '#EFD469';
    let Colors = JSON.parse(JSON.stringify(boxColors));
    Colors[winIdxOne].backgroundColor = winningColor;
    Colors[winIdxTwo].backgroundColor = winningColor;
    Colors[winIdxThree].backgroundColor = winningColor;
    changeBoxColors(Colors);
    changeInfoDisplay(show.YOU_LOST);
    addToComputerScore();
    setTimeout(this.restartGame, 3000);
  }

  restartGame () {
    const { boxColors, changeBoxColors, updateTheBoard, updateTurnNumber } = this.props;
    let boardCopy = _.cloneDeep(boxColors);
    let freshBoard = boardCopy.map((eachColor) => {
      eachColor.backgroundColor = '#D2D2D2';
      return eachColor;
    });
    changeBoxColors(freshBoard);
    updateTheBoard(cleanBoard);
    updateTurnNumber(1);
    this.whoStarts();
  }

  computerMove () {
    const { turnNumber, updateTheBoard, tokens: { playerToken, computerToken } } = this.props;
    let gameBoard = this.props.gameBoard.slice();

    if (turnNumber === 1) {
      gameBoard[0] = computerToken;
    }
    if (turnNumber === 2) {
      if (gameBoard[4] === '') {
        gameBoard[4] = computerToken;
      } else gameBoard[2] = computerToken;
    }

    if (turnNumber === 3) {
      if (gameBoard[4] === '') {
        gameBoard[4] = computerToken;
      } else gameBoard[8] = computerToken;
    }

    if (turnNumber === 4) {
      gameBoard = rowLogic(gameBoard, playerToken, computerToken);

      if (gameBoard.join('').length === 3) {
        gameBoard = forkLogic(gameBoard, playerToken, computerToken);
      }
      if (gameBoard.join('').length === 3) {
        if (gameBoard[1] !== playerToken) {
          gameBoard[1] = computerToken;
        } else gameBoard[3] = computerToken;
      }
    }
    if (turnNumber > 4) {
      gameBoard = rowLogic(gameBoard, computerToken, computerToken);

      if (gameBoard.join('').length === turnNumber - 1) {
        gameBoard = rowLogic(gameBoard, playerToken, computerToken);
      }
      if (gameBoard.join('').length === turnNumber - 1) {
        let i = 0;
        while (gameBoard[i] !== '') i += 1;
        gameBoard[i] = computerToken;
      }
    }

    updateTheBoard(gameBoard);
    this.checkIfWinner(gameBoard);
  }

  render () {
    if (this.props.introScreen) {
      return (
        <IntroScreen
          className="IntroScreen"
          chooseThis={this.playerHasChosen}
        />
      );
    } else return (

        <InfoDisplay
          info={this.props.infoDisplay}
        />
        <GameBoard
          playersTurn={this.props.playersTurn}
          nextMove={this.newPlayerMove}
          squareContains={this.props.gameBoard}
          boxColors={this.props.boxColors}
        />
        <ScoreBoard
          compScore={this.props.computerScore}
        />

    );
  }
}

TicTacToe.propTypes = {

  /* action creators */

  changeInfoDisplay: PropTypes.func.isRequired,
  showIntroScreen: PropTypes.func.isRequired,
  addToComputerScore: PropTypes.func.isRequired,
  changeWhosTurn: PropTypes.func.isRequired,
  updateTurnNumber: PropTypes.func.isRequired,
  whoStartsNext: PropTypes.func.isRequired,
  setTokens: PropTypes.func.isRequired,
  changeBoxColors: PropTypes.func.isRequired,
  updateTheBoard: PropTypes.func.isRequired,

  /* properties */

  infoDisplay: PropTypes.string.isRequired,
  introScreen: PropTypes.bool.isRequired,
  computerScore: PropTypes.number.isRequired,
  playersTurn: PropTypes.bool.isRequired,
  turnNumber: PropTypes.number.isRequired,
  playerStarts: PropTypes.bool.isRequired,
  tokens: PropTypes.shape({
    playerToken: PropTypes.string.isRequired,
    computerToken: PropTypes.string.isRequired,
  }).isRequired,
  boxColors: PropTypes.arrayOf(PropTypes.shape({
    backgroundColor: PropTypes.string.isRequired,
  }).isRequired).isRequired,
  gameBoard: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
};

/* eslint-disable func-style */

function mapStateToProps ({
  infoDisplay,
  introScreen,
  computerScore,
  playersTurn,
  turnNumber,
  playerStarts,
  tokens,
  boxColors,
  gameBoard,
}) {
  return {
    infoDisplay,
    introScreen,
    computerScore,
    playersTurn,
    turnNumber,
    playerStarts,
    tokens,
    boxColors,
    gameBoard,
  };
}

function mapDispatchToProps (dispatch) {
  return bindActionCreators(Object.assign({}, actions), dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(TicTacToe);
<|endoftext|>