<commit_before>import _ from 'lodash';
import React from 'react';
import GenericLoader from '../components/GenericLoader';
import ExperimentalBanner from '../components/ExperimentalBanner';
import SectionHeading from '../components/SectionHeading';
import Button from '../components/Button';
import {apiFetchJson} from '../helpers/apiFetchJson';
import ProfileBarChart from '../student_profile/ProfileBarChart.js';
import * as Routes from '../helpers/Routes';

const style = {
  container: {
    margin: 30
  },
  studentRow: {
    marginBottom: 30,
  },
  profileBarChartStyles: {
    title: {
      fontSize: 20
    },
    container: {
      width: '100%',
      margin: '10px auto',
      position: 'relative'
    },
    chartHeight: 250,
  },
  a: {
    fontSize: 24
  },
  buttonStyle: {
    float: 'left',
    margin: 5,
  },
  buttonSectionStyle: {
    width: '100%',
    height: 50
  }
};

class IsServiceWorking extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      serviceTypeSelected: null // options: null, 502, 503
    };

    this.fetchIsServiceWorkingData = this.fetchIsServiceWorkingData.bind(this);
    this.renderPage = this.renderPage.bind(this);
  }

  render() {
    return (

        <ExperimentalBanner />
        <SectionHeading>
          "Is this Service Working for this Student?"
        </SectionHeading>

          Every student is different.
          Some students might respond well to a service, while others
          respond poorly to the same service.

          Use the buttons below to explore how students may have responded to
          services assigned to them over the course of the past school year.

        {this.renderButtons()}
        {this.renderCharts()}

    );
  }

  renderButtons() {
    const attendanceOfficer = {
      id: 502,
      name: 'Attendance Officer',
    };

    const attendanceContract = {
      id: 503,
      name: 'Attendance Contract',
    };

    return (

        <Button style={style.buttonStyle}
          onClick={() => {
            this.setState({serviceTypeSelected: null}, () => {
              this.setState({serviceTypeSelected: attendanceOfficer})
            })
          }}>
          Attendance Officer
        </Button>
        <Button style={style.buttonStyle}
          onClick={() => {
            this.setState({serviceTypeSelected: null}, () => {
              this.setState({serviceTypeSelected: attendanceContract})
            })
          }}>
          Attendance Contract
        </Button>

    );
  }

  renderCharts() {
    const {serviceTypeSelected} = this.state;

    if (!serviceTypeSelected) return null;

    return (
      <GenericLoader
        promiseFn={this.fetchIsServiceWorkingData}
        render={this.renderPage} />
    );
  }

  fetchIsServiceWorkingData() {
    const {serviceTypeSelected} = this.state;

    const url = `/api/is_service_working_json/${serviceTypeSelected.id}`;

    return apiFetchJson(url);
  }

  renderPage(json) {
    return (

        {this.renderStudents(json)}

    );
  }

  renderPhasebands(services) {
    return services.map(service => {
      return {
        momentUTCstart: moment.utc(service.date_started),
        momentUTCend: moment.utc(service.discontinued_at),
        text: this.state.serviceTypeSelected.name
      };
    }, this);
  }

  renderStudents(json) {
    const chartData = json.chart_data;
    const sortedData = _.sortBy(chartData, datum => {
      return (datum.services.length > 0)
        ? datum.services[0].date_started
        : 0;
    });

    return sortedData.map((datum) => {
      const student = datum.student;
      const services = datum.services;
      const phasebands = this.renderPhasebands(services);

      return (

            {student.first_name} {student.last_name}

          ({datum.school} Grade {student.grade})
          <ProfileBarChart
            events={datum.absences}
            titleText="Absences"
            monthsBack={24}
            phasebands={phasebands}
            hideBackToTop={true}
            styles={style.profileBarChartStyles} />

      );
    }, this);

  }

}

IsServiceWorking.propTypes = {
  serializedData: React.PropTypes.object.isRequired,
}

export default IsServiceWorking;
<commit_msg>Fix sort func<commit_after>import _ from 'lodash';
import React from 'react';
import GenericLoader from '../components/GenericLoader';
import ExperimentalBanner from '../components/ExperimentalBanner';
import SectionHeading from '../components/SectionHeading';
import Button from '../components/Button';
import {apiFetchJson} from '../helpers/apiFetchJson';
import ProfileBarChart from '../student_profile/ProfileBarChart.js';
import * as Routes from '../helpers/Routes';

const style = {
  container: {
    margin: 30
  },
  studentRow: {
    marginBottom: 30,
  },
  profileBarChartStyles: {
    title: {
      fontSize: 20
    },
    container: {
      width: '100%',
      margin: '10px auto',
      position: 'relative'
    },
    chartHeight: 250,
  },
  a: {
    fontSize: 24
  },
  buttonStyle: {
    float: 'left',
    margin: 5,
  },
  buttonSectionStyle: {
    width: '100%',
    height: 50
  }
};

class IsServiceWorking extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      serviceTypeSelected: null // options: null, 502, 503
    };

    this.fetchIsServiceWorkingData = this.fetchIsServiceWorkingData.bind(this);
    this.renderPage = this.renderPage.bind(this);
  }

  render() {
    return (

        <ExperimentalBanner />
        <SectionHeading>
          "Is this Service Working for this Student?"
        </SectionHeading>

          Every student is different.
          Some students might respond well to a service, while others
          respond poorly to the same service.

          Use the buttons below to explore how students may have responded to
          services assigned to them over the course of the past school year.

        {this.renderButtons()}
        {this.renderCharts()}

    );
  }

  renderButtons() {
    const attendanceOfficer = {
      id: 502,
      name: 'Attendance Officer',
    };

    const attendanceContract = {
      id: 503,
      name: 'Attendance Contract',
    };

    return (

        <Button style={style.buttonStyle}
          onClick={() => {
            this.setState({serviceTypeSelected: null}, () => {
              this.setState({serviceTypeSelected: attendanceOfficer})
            })
          }}>
          Attendance Officer
        </Button>
        <Button style={style.buttonStyle}
          onClick={() => {
            this.setState({serviceTypeSelected: null}, () => {
              this.setState({serviceTypeSelected: attendanceContract})
            })
          }}>
          Attendance Contract
        </Button>

    );
  }

  renderCharts() {
    const {serviceTypeSelected} = this.state;

    if (!serviceTypeSelected) return null;

    return (
      <GenericLoader
        promiseFn={this.fetchIsServiceWorkingData}
        render={this.renderPage} />
    );
  }

  fetchIsServiceWorkingData() {
    const {serviceTypeSelected} = this.state;

    const url = `/api/is_service_working_json/${serviceTypeSelected.id}`;

    return apiFetchJson(url);
  }

  renderPage(json) {
    return (

        {this.renderStudents(json)}

    );
  }

  renderPhasebands(services) {
    return services.map(service => {
      return {
        momentUTCstart: moment.utc(service.date_started),
        momentUTCend: moment.utc(service.discontinued_at),
        text: this.state.serviceTypeSelected.name
      };
    }, this);
  }

  renderStudents(json) {
    const chartData = json.chart_data;
    const sortedData = _.sortBy(chartData, datum => {
      return (datum.services.length > 0)
        ? _.map(datum.services, 'date_started').sort()[0]
        : 0;
    });

    return sortedData.map((datum) => {
      const student = datum.student;
      const services = datum.services;
      const phasebands = this.renderPhasebands(services);

      return (

            {student.first_name} {student.last_name}

          ({datum.school} Grade {student.grade})
          <ProfileBarChart
            events={datum.absences}
            titleText="Absences"
            monthsBack={24}
            phasebands={phasebands}
            hideBackToTop={true}
            styles={style.profileBarChartStyles} />

      );
    }, this);

  }

}

IsServiceWorking.propTypes = {
  serializedData: React.PropTypes.object.isRequired,
}

export default IsServiceWorking;
<|endoftext|>


<commit_before>const React = require('react');

const Container = React.createClass({
  propTypes: {
    fluid: React.PropTypes.bool
  },

  getDefaultProps () {
    return {
      fluid: true
    };
  },

  render () {
    return (

        {this.props.children}

    );
  }
});

module.exports = Container;<commit_msg>Adding a styles prop to the Container component<commit_after>const React = require('react');

const Container = React.createClass({
  propTypes: {
    fluid: React.PropTypes.bool,
    styles: React.PropTypes.object
  },

  getDefaultProps () {
    return {
      fluid: true
    };
  },

  render () {
    return (

        {this.props.children}

    );
  }
});

module.exports = Container;
<|endoftext|>


<commit_before>import React from 'react';
import TestUtils from 'react-dom/test-utils';
import { shallow } from 'enzyme';
import NumberComponent from './../number';
import Pager from './pager';
import { elementsTagTest, rootTagTest } from '../../utils/helpers/tags/tags-specs';

describe('Pager', () => {
  let instance, instance2, spy1, spy2;

  beforeEach(() => {
    spy1 = jasmine.createSpy('instance 1 pagination');
    spy2 = jasmine.createSpy('instance 2 pagination');

    instance = TestUtils.renderIntoDocument(
      <Pager
        currentPage='1'
        pageSize='10'
        showPageSizeSelection={ true }
        totalRecords='100'
        onPagination={ spy1 }
      />
    );

    instance2 = TestUtils.renderIntoDocument(
      <Pager
        currentPage='2'
        pageSize='10'
        totalRecords='20'
        onPagination={ spy2 }
      />
    );
  });

  describe('onInitialise', () => {
    it('sets the currentPage within the state', () => {
      expect(instance.state.currentPage).toEqual('1');
    });
  });

  describe('componentWillReceiveProps', () => {
    it('updates the internal state currentPage with the props currentPage', () => {
      instance.setState({ currentPage: '2' });
      instance.componentWillReceiveProps({ currentPage: '1' });

      expect(instance.state.currentPage).toEqual('1');
    });
  });

  describe('handleCurrentPageKeyUp', () => {

    beforeEach(() => {
      spyOn(instance, 'emitChangeCallback')
    });

    describe('when the event is a enter key', () => {
      it('calls emitChangeCallback', () => {
        let ev = { which: 13 };
        instance.handleCurrentPageKeyUp(ev);
        expect(instance.emitChangeCallback).toHaveBeenCalledWith('input', ev);
      });
    });

    describe('when the the event is any other key', () => {
      it('does not emit the change', () => {
        let ev = { which: 8 };
        instance.handleCurrentPageKeyUp(ev);
        expect(instance.emitChangeCallback).not.toHaveBeenCalled()
      });
    });
  });

  describe('emitChangeCallback', () => {
    describe('when element is next', () => {
      it('emits onPagination increasing currentPage by 1', () => {
        instance.emitChangeCallback('next', {});
        expect(spy1).toHaveBeenCalledWith('2', '10', 'next');
      });
    });

    describe('when element is input', () => {
      it('emit a new page from the input field', () => {
        let event = { target: { value: '5' } };
        instance.emitChangeCallback('input', event);
        expect(spy1).toHaveBeenCalledWith('5', '10', 'input');
      });

      describe('when input is greater than the max page number', () => {
        it('emit a max page as the new current page', () => {
          let event = { target: { value: '100' } };
          instance.emitChangeCallback('input', event);
          expect(spy1).toHaveBeenCalledWith('10', '10', 'input');
        });
      });

      describe('when input is less than 1', () => {
        it('emit first page as the new current page', () => {
          let event = { target: { value: '0' } };
          instance.emitChangeCallback('input', event);
          expect(spy1).toHaveBeenCalledWith('1', '10', 'input');
        });
      });

      describe('when input is blank', () => {
        it('resets currentPage to the currentPage is state', () => {
          spyOn(instance, 'setState');
          let event = { target: { value: '' } };
          instance.emitChangeCallback('input', event);

          expect(instance.setState).toHaveBeenCalledWith({
            currentPage: instance.state.currentPage
          });
        });
      });
    });

    describe('when element is not incorrect', () => {
      it('does nt call onPagination', () => {
        instance.emitChangeCallback('', {});
        expect(spy1).not.toHaveBeenCalled();
      });
    });

    describe('when element is previous', () => {
      it('emits onPagination decreasing currentPage by 1', () => {
        instance2.emitChangeCallback('previous', {});
        expect(spy2).toHaveBeenCalledWith('1', '10', 'previous');
      });
    });

    describe('when element is size', () => {
      it('emits the new page size', () => {
        let event = { target: { value: '50' } };
        instance.emitChangeCallback('size', event);
        expect(spy1).toHaveBeenCalledWith('1', '50', 'size');
      });

      describe('when page size is not a correct option', () => {
        it('does not emit a callback', () => {
          let event = { target: { value: '13' } };
          instance.emitChangeCallback('size', event);
          expect(spy1).not.toHaveBeenCalled();
        });
      });

      describe('when not on the first page', () => {
        // TODO: see page.js 155
      });
    });
  });

  describe('maxPage', () => {
    it('returns the max page depending on totalRecords and pageSize', () => {
      expect(instance.maxPage).toEqual(10);
    });

    describe('when total records is 0', () => {
      it('defaults the maxPage to 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='10'
            totalRecords='0'
            onPagination={ spy1 }
          />
        );
        expect(instance.maxPage).toEqual(1);
      });
    });

    describe('when pageSize is undefined', () => {
      it('returns 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize={ null }
            showPageSizeSelection={ true }
            totalRecords='100'
            onPagination={ spy1 }
          />
        );

        expect(instance.maxPage).toEqual(1);
      });
    });

    describe('when pageSize is 0', () => {
      it('returns 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='0'
            showPageSizeSelection={ true }
            totalRecords='100'
            onPagination={ spy1 }
          />
        );

        expect(instance.maxPage).toEqual(1);
      });
    });
  });

  describe('disablePrevious', () => {
    describe('when currentPage is 1', () => {
      it('returns true', () => {
        expect(instance.disablePrevious).toBeTruthy();
      });
    });

    describe('when currentPage is not 1', () => {
      it('returns false', () => {
        expect(instance2.disablePrevious).toBeFalsy();
      });
    });
  });

  describe('disableNext', () => {
    describe('showing the last record', () => {
      it('returns true', () => {
        expect(instance2.disableNext).toBeTruthy();
      });
    });

    describe('when currentPage is not 1', () => {
      it('returns false', () => {
        expect(instance.disableNext).toBeFalsy();
      });
    });
  });

  describe('previousArrow', () => {
    let previous;

    it('returns an arrow icon', () => {
      previous = instance.previousArrow;
      expect(previous.props.type).toEqual('dropdown');
    });

    describe('when disabled', () => {
      it('adds a disabled class', () => {
        previous = instance.previousArrow;
        expect(previous.props.className).toEqual('carbon-pager__previous carbon-pager__previous--disabled');
      });
    });

    describe('when enabled', () => {
      it('adds a onClick handler', () => {
        let input = TestUtils.findRenderedDOMComponentWithClass(instance2, 'carbon-pager__previous');
        TestUtils.Simulate.click(input);
        expect(spy2).toHaveBeenCalled();
      });
    });
  });

  describe('currentPageInput', () => {
    let input;

    it('returns a number component', () => {
      expect(TestUtils.findRenderedComponentWithType(instance, NumberComponent)).toBeTruthy();
    });

    it('sets the current page to this.state.currentPage', () => {
      input = instance.currentPageInput;
      expect(input.props.value).toEqual(instance.props.currentPage);
    });

    it('adds a onChange handler', () => {
      spyOn(instance, 'setState');
      let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-number__input');
      TestUtils.Simulate.change(input);
      expect(instance.setState).toHaveBeenCalled();
    });

    it('adds a onBlur handler', () => {
      let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-number__input');
      TestUtils.Simulate.blur(input);
      expect(spy1).toHaveBeenCalled();
    });
  });

  describe('nextArrow', () => {
    let next;

    it('returns a arrow icon', () => {
      next = instance.nextArrow;
      expect(next.props.type).toEqual('dropdown');
    });

    describe('when disabled', () => {
      it('adds a disabled class', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='10'
            totalRecords='1'
            onPagination={ spy1 }
          />
        );

        next = instance.nextArrow;
        expect(next.props.className).toEqual('carbon-pager__next carbon-pager__next--disabled');
      });
    });

    describe('when enabled', () => {
      it('adds a onClick handler', () => {
        let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-pager__next');
        TestUtils.Simulate.click(input);
        expect(spy1).toHaveBeenCalled();
      });
    });
  });

  describe('sizeSelectionDropdown', () => {
    describe('when showPageSizeSelection is true', () => {
      let dropdown;

      it('returns size dropdown', () => {
        let size = instance.sizeSelectionDropdown;
        dropdown = size.props.children[1];
        expect(dropdown.props.value).toEqual(instance.props.pageSize);
      });

      it('adds a onChange event to the dropdown', () => {
        dropdown = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-dropdown__input');
        TestUtils.Simulate.change(dropdown);
        expect(spy1).toHaveBeenCalled();
      });
    });

    describe('when showPageSizeSelection is false', () => {
      it('does not return the dropdown', () => {
        expect(instance2.sizeSelectionDropdown).toBeFalsy();
      });
    });

    it('adds a class of unselectable', () => {
      expect(TestUtils.scryRenderedDOMComponentsWithClass(instance, 'unselectable').length).toBeTruthy();
    });
  });

  describe('tags on component', () => {
    let wrapper = shallow(
      <Pager
        currentPage='1'
        data-element='bar'
        data-role='baz'
        onPagination={ ()=>{} }
        totalRecords='100'
      />
    );

    it('includes correct component, element and role data tags', () => {
      rootTagTest(wrapper, 'pager', 'bar', 'baz');
    });

    describe('on internal elements', () => {
      let wrapper = shallow(
        <Pager
          currentPage='1'
          onPagination={ ()=>{} }
          pageSize='10'
          showPageSizeSelection={ true }
          totalRecords='100'
        />
      );
      elementsTagTest(wrapper, [
        'current-page',
        'next-page',
        'page-select',
        'previous-page'
      ]);
    });
  });
});
<commit_msg>fix spec typo<commit_after>import React from 'react';
import TestUtils from 'react-dom/test-utils';
import { shallow } from 'enzyme';
import NumberComponent from './../number';
import Pager from './pager';
import { elementsTagTest, rootTagTest } from '../../utils/helpers/tags/tags-specs';

describe('Pager', () => {
  let instance, instance2, spy1, spy2;

  beforeEach(() => {
    spy1 = jasmine.createSpy('instance 1 pagination');
    spy2 = jasmine.createSpy('instance 2 pagination');

    instance = TestUtils.renderIntoDocument(
      <Pager
        currentPage='1'
        pageSize='10'
        showPageSizeSelection={ true }
        totalRecords='100'
        onPagination={ spy1 }
      />
    );

    instance2 = TestUtils.renderIntoDocument(
      <Pager
        currentPage='2'
        pageSize='10'
        totalRecords='20'
        onPagination={ spy2 }
      />
    );
  });

  describe('onInitialise', () => {
    it('sets the currentPage within the state', () => {
      expect(instance.state.currentPage).toEqual('1');
    });
  });

  describe('componentWillReceiveProps', () => {
    it('updates the internal state currentPage with the props currentPage', () => {
      instance.setState({ currentPage: '2' });
      instance.componentWillReceiveProps({ currentPage: '1' });

      expect(instance.state.currentPage).toEqual('1');
    });
  });

  describe('handleCurrentPageKeyUp', () => {

    beforeEach(() => {
      spyOn(instance, 'emitChangeCallback')
    });

    describe('when the event is a enter key', () => {
      it('calls emitChangeCallback', () => {
        let ev = { which: 13 };
        instance.handleCurrentPageKeyUp(ev);
        expect(instance.emitChangeCallback).toHaveBeenCalledWith('input', ev);
      });
    });

    describe('when the the event is any other key', () => {
      it('does not emit the change', () => {
        let ev = { which: 8 };
        instance.handleCurrentPageKeyUp(ev);
        expect(instance.emitChangeCallback).not.toHaveBeenCalled()
      });
    });
  });

  describe('emitChangeCallback', () => {
    describe('when element is next', () => {
      it('emits onPagination increasing currentPage by 1', () => {
        instance.emitChangeCallback('next', {});
        expect(spy1).toHaveBeenCalledWith('2', '10', 'next');
      });
    });

    describe('when element is input', () => {
      it('emit a new page from the input field', () => {
        let event = { target: { value: '5' } };
        instance.emitChangeCallback('input', event);
        expect(spy1).toHaveBeenCalledWith('5', '10', 'input');
      });

      describe('when input is greater than the max page number', () => {
        it('emit a max page as the new current page', () => {
          let event = { target: { value: '100' } };
          instance.emitChangeCallback('input', event);
          expect(spy1).toHaveBeenCalledWith('10', '10', 'input');
        });
      });

      describe('when input is less than 1', () => {
        it('emit first page as the new current page', () => {
          let event = { target: { value: '0' } };
          instance.emitChangeCallback('input', event);
          expect(spy1).toHaveBeenCalledWith('1', '10', 'input');
        });
      });

      describe('when input is blank', () => {
        it('resets currentPage to the currentPage is state', () => {
          spyOn(instance, 'setState');
          let event = { target: { value: '' } };
          instance.emitChangeCallback('input', event);

          expect(instance.setState).toHaveBeenCalledWith({
            currentPage: instance.state.currentPage
          });
        });
      });
    });

    describe('when element is not incorrect', () => {
      it('does not call onPagination', () => {
        instance.emitChangeCallback('', {});
        expect(spy1).not.toHaveBeenCalled();
      });
    });

    describe('when element is previous', () => {
      it('emits onPagination decreasing currentPage by 1', () => {
        instance2.emitChangeCallback('previous', {});
        expect(spy2).toHaveBeenCalledWith('1', '10', 'previous');
      });
    });

    describe('when element is size', () => {
      it('emits the new page size', () => {
        let event = { target: { value: '50' } };
        instance.emitChangeCallback('size', event);
        expect(spy1).toHaveBeenCalledWith('1', '50', 'size');
      });

      describe('when page size is not a correct option', () => {
        it('does not emit a callback', () => {
          let event = { target: { value: '13' } };
          instance.emitChangeCallback('size', event);
          expect(spy1).not.toHaveBeenCalled();
        });
      });

      describe('when not on the first page', () => {
        // TODO: see page.js 155
      });
    });
  });

  describe('maxPage', () => {
    it('returns the max page depending on totalRecords and pageSize', () => {
      expect(instance.maxPage).toEqual(10);
    });

    describe('when total records is 0', () => {
      it('defaults the maxPage to 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='10'
            totalRecords='0'
            onPagination={ spy1 }
          />
        );
        expect(instance.maxPage).toEqual(1);
      });
    });

    describe('when pageSize is undefined', () => {
      it('returns 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize={ null }
            showPageSizeSelection={ true }
            totalRecords='100'
            onPagination={ spy1 }
          />
        );

        expect(instance.maxPage).toEqual(1);
      });
    });

    describe('when pageSize is 0', () => {
      it('returns 1', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='0'
            showPageSizeSelection={ true }
            totalRecords='100'
            onPagination={ spy1 }
          />
        );

        expect(instance.maxPage).toEqual(1);
      });
    });
  });

  describe('disablePrevious', () => {
    describe('when currentPage is 1', () => {
      it('returns true', () => {
        expect(instance.disablePrevious).toBeTruthy();
      });
    });

    describe('when currentPage is not 1', () => {
      it('returns false', () => {
        expect(instance2.disablePrevious).toBeFalsy();
      });
    });
  });

  describe('disableNext', () => {
    describe('showing the last record', () => {
      it('returns true', () => {
        expect(instance2.disableNext).toBeTruthy();
      });
    });

    describe('when currentPage is not 1', () => {
      it('returns false', () => {
        expect(instance.disableNext).toBeFalsy();
      });
    });
  });

  describe('previousArrow', () => {
    let previous;

    it('returns an arrow icon', () => {
      previous = instance.previousArrow;
      expect(previous.props.type).toEqual('dropdown');
    });

    describe('when disabled', () => {
      it('adds a disabled class', () => {
        previous = instance.previousArrow;
        expect(previous.props.className).toEqual('carbon-pager__previous carbon-pager__previous--disabled');
      });
    });

    describe('when enabled', () => {
      it('adds a onClick handler', () => {
        let input = TestUtils.findRenderedDOMComponentWithClass(instance2, 'carbon-pager__previous');
        TestUtils.Simulate.click(input);
        expect(spy2).toHaveBeenCalled();
      });
    });
  });

  describe('currentPageInput', () => {
    let input;

    it('returns a number component', () => {
      expect(TestUtils.findRenderedComponentWithType(instance, NumberComponent)).toBeTruthy();
    });

    it('sets the current page to this.state.currentPage', () => {
      input = instance.currentPageInput;
      expect(input.props.value).toEqual(instance.props.currentPage);
    });

    it('adds a onChange handler', () => {
      spyOn(instance, 'setState');
      let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-number__input');
      TestUtils.Simulate.change(input);
      expect(instance.setState).toHaveBeenCalled();
    });

    it('adds a onBlur handler', () => {
      let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-number__input');
      TestUtils.Simulate.blur(input);
      expect(spy1).toHaveBeenCalled();
    });
  });

  describe('nextArrow', () => {
    let next;

    it('returns a arrow icon', () => {
      next = instance.nextArrow;
      expect(next.props.type).toEqual('dropdown');
    });

    describe('when disabled', () => {
      it('adds a disabled class', () => {
        instance = TestUtils.renderIntoDocument(
          <Pager
            currentPage='1'
            pageSize='10'
            totalRecords='1'
            onPagination={ spy1 }
          />
        );

        next = instance.nextArrow;
        expect(next.props.className).toEqual('carbon-pager__next carbon-pager__next--disabled');
      });
    });

    describe('when enabled', () => {
      it('adds a onClick handler', () => {
        let input = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-pager__next');
        TestUtils.Simulate.click(input);
        expect(spy1).toHaveBeenCalled();
      });
    });
  });

  describe('sizeSelectionDropdown', () => {
    describe('when showPageSizeSelection is true', () => {
      let dropdown;

      it('returns size dropdown', () => {
        let size = instance.sizeSelectionDropdown;
        dropdown = size.props.children[1];
        expect(dropdown.props.value).toEqual(instance.props.pageSize);
      });

      it('adds a onChange event to the dropdown', () => {
        dropdown = TestUtils.findRenderedDOMComponentWithClass(instance, 'carbon-dropdown__input');
        TestUtils.Simulate.change(dropdown);
        expect(spy1).toHaveBeenCalled();
      });
    });

    describe('when showPageSizeSelection is false', () => {
      it('does not return the dropdown', () => {
        expect(instance2.sizeSelectionDropdown).toBeFalsy();
      });
    });

    it('adds a class of unselectable', () => {
      expect(TestUtils.scryRenderedDOMComponentsWithClass(instance, 'unselectable').length).toBeTruthy();
    });
  });

  describe('tags on component', () => {
    let wrapper = shallow(
      <Pager
        currentPage='1'
        data-element='bar'
        data-role='baz'
        onPagination={ ()=>{} }
        totalRecords='100'
      />
    );

    it('includes correct component, element and role data tags', () => {
      rootTagTest(wrapper, 'pager', 'bar', 'baz');
    });

    describe('on internal elements', () => {
      let wrapper = shallow(
        <Pager
          currentPage='1'
          onPagination={ ()=>{} }
          pageSize='10'
          showPageSizeSelection={ true }
          totalRecords='100'
        />
      );
      elementsTagTest(wrapper, [
        'current-page',
        'next-page',
        'page-select',
        'previous-page'
      ]);
    });
  });
});
<|endoftext|>


<commit_before>/*------------------------------------------
HeatChart class
Generate a canvas based HeatChart. This class will generate
a canvas, so canvas does not need to be defined in html file
before using. Simply new it, specify the dom you want to append
the heat map to and provide other required arguments.
usage:
new HeatChart(targetElement, dotArray, canvasWidth, canvasHeight, gradientMode);
arguments:
    targetElement: the dom you want to append this HeatChart into;
    dotArray: data to generate HeatChart, is an array of {x:x,y:y}, can be scaled
              in percentage or in pixel, needs to be specified by 'scalemode'.
    scaleMode: 0 for percentage scale mode, other for pixel scale mode
    canvasWidth: pre-defined canvas width;
    canvasHeight: pre-defined canvas height;
    gradientMode: color mapping mode, currently choose from 0 - 5;
    transparency: canvas transparency so you can lay it on a picture;
    displayMode: 0 for heat map, 1 for gray scale map, 2 for dots
------------------------------------------*/
var HeatChart = (function() {
    //constructor
    function HeatChart(targetElement , dotArray , configuration){
        this.dotArray = dotArray;

        this.scaleMode = (configuration.scaleMode == undefined ? 0 : configuration.scaleMode);
        this.canvasWidth = ((configuration.canvasWidth == undefined || configuration.canvasWidth == 0) ? targetElement.offsetWidth : configuration.canvasWidth);
        this.canvasHeight = ((configuration.canvasHeight == undefined || configuration.canvasHeight == 0) ? targetElement.offsetHeight : configuration.canvasHeight);
        this.gradientMode = (configuration.gradientMode == undefined ? 0 : configuration.gradientMode);
        this.gradientRadius = ((configuration.gradientRadius == undefined || configuration.gradientRadius == 0) ? 32 : configuration.gradientRadius);
        console.debug(this.gradientRadius);
        this.transparency = ((configuration.transparency == undefined || configuration.transparency == 0) ? 0.5 : configuration.transparency);
        this.displayMode = (configuration.displayMode == undefined ? 0 : configuration.displayMode);

        //decide grayScaleAlpha based on number of dots
        //grayScaleAlpha is the start color of one gradient gray scale dot
        if(this.dotArray.length <= 100) this.grayScaleAlpha = 0.2;
        else if(this.dotArray.length >= 2000) this.grayScaleAlpha = 0.048;
        else this.grayScaleAlpha = 0.208 - this.dotArray.length * 0.00008;

        //generate a canvas in targetElement dom to display HeatChart
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;
        this.canvas.style.position = 'absolute';
        targetElement.appendChild(this.canvas);

        //supported color gradient mode for color mapping
        this.gradientRainbow = {
            0:      'rgba(0,0,0,0.0)',
            0.14:   'rgba(139,0,255,' + this.transparency + ')',
            0.28:   'rgba(0,0,255,' + this.transparency + ')',
            0.42:   'rgba(0,127,255,' + this.transparency + ')',
            0.56:   'rgba(0,255,0,' + this.transparency + ')',
            0.70:   'rgba(255,255,0,' + this.transparency + ')',
            0.84:   'rgba(255,164,0,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        }
        this.gradientWarm = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(255,255,153,' + this.transparency + ')',
            0.7:    'rgba(255,153,51,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        };
        this.gradientCold = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(102,255,102,' + this.transparency + ')',
            0.7:    'rgba(0,51,153,' + this.transparency + ')',
            1.0:    'rgba(153,0,204,' + this.transparency + ')'
        };
        this.gradientBurn = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(255,255,102,' + this.transparency + ')',
            0.7:    'rgba(255,102,0,' + this.transparency + ')',
            1.0:    'rgba(153,51,153,' + this.transparency + ')'
         };
        this.gradientLight = {
            0:      'rgba(0,0,0,0.0)',
            0.3:    'rgba(153,255,102,' + this.transparency + ')',
            0.5:    'rgba(255,255,0,' + this.transparency + ')',
            0.75:   'rgba(255,164,0,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        };

        if(this.displayMode == 2){
            this.drawDots();
        }else if (this.displayMode == 1){
            this.drawCircles();
        }else if (this.displayMode == 0){
            this.drawCircles();
            this.grayToColor();
        }
    }

    //create color spectrum canvas and return its data
    HeatChart.prototype.createColorSpectrum = function(){
        var gradientConfiguration;
        switch(this.gradientMode){
            case 0:
            gradientConfiguration = this.gradientRainbow;
            break;
            case 1:
            gradientConfiguration = this.gradientWarm;
            break;
            case 2:
            gradientConfiguration = this.gradientCold;
            break;
            case 3:
            gradientConfiguration = this.gradientBurn;
            break;
            case 4:
            gradientConfiguration = this.gradientLight;
            break;
            default:
            gradientConfiguration = this.gradientRaindow;
            break;
        }

        var spectrumCanvas = document.createElement('canvas');
        var spectrumCtx = spectrumCanvas.getContext('2d');
        spectrumCanvas.width = 256;
        spectrumCanvas.height = 1;

        var gradient = spectrumCtx.createLinearGradient(0 , 0 , 256 , 1);
        for (var colorStop in gradientConfiguration)
            gradient.addColorStop(colorStop , gradientConfiguration[colorStop]);
        spectrumCtx.fillStyle = gradient;
        spectrumCtx.fillRect(0 , 0 , 256 , 1);
        return spectrumCtx.getImageData(0 , 0 , 256 , 1).data;
    }

    //convert percentage coordinates to pixels
    HeatChart.prototype.toPixel = function(dot){
        return{
            x: dot.x / 100 * this.canvasWidth,
            y: dot.y / 100 * this.canvasHeight
        }
    }

    // draw a dot based on given coordinate, called by this.drawDots()
    HeatChart.prototype.drawOneDot = function(x , y){
        this.ctx.beginPath();
        this.ctx.arc(x , y , 3 , 0 , 2*Math.PI , false);
        this.ctx.fill();
    }

    //draw dots data as black dots on the canvas for test usage
    HeatChart.prototype.drawDots = function(){
        this.ctx.fillStyle = '#000000';
        var dotInPixel;
        for(var i = 0; i < this.dotArray.length ; i++){
            if(this.scaleMode == 0){
                dotInPixel = this.toPixel(this.dotArray[i]);
                this.drawOneDot(dotInPixel.x, dotInPixel.y);
            }else{
                this.drawOneDot(this.dotArray[i]);
            }
        }
    }

    //draw a transtargetElement radial gradient circle
    HeatChart.prototype.drawOneCircle = function(dot){
        var gradient = this.ctx.createRadialGradient(dot.x , dot.y , 1 , dot.x , dot.y , this.gradientRadius);
        this.ctx.beginPath();
        gradient.addColorStop(0 , 'rgba(0,0,0,' + this.grayScaleAlpha + ')');
        gradient.addColorStop(1.0 , 'rgba(0,0,0,0)');
        this.ctx.fillStyle = gradient;
        this.ctx.arc(dot.x , dot.y , this.gradientRadius , 2*Math.PI , false);
        this.ctx.fill();
    }

    //draw circles for each dot
    HeatChart.prototype.drawCircles = function(){
        var dotInPixel;
        for(var i = 0 ; i < this.dotArray.length ; i++){
            if(this.scaleMode == 0){
                dotInPixel = this.toPixel(this.dotArray[i]);
                this.drawOneCircle(dotInPixel);
            }else{
                this.drawOneCircle(this.dotArray[i]);
            }
        }
    }

    //convert grayscale image to color by mapping the color on colorband
    HeatChart.prototype.grayToColor = function(){
        var imageDataObject = this.ctx.getImageData(0 , 0 , this.canvasWidth , this.canvasHeight);
        var alphaMax = 0;
        var alpha;
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            alpha = imageDataObject.data[i + 3];
            if (alpha > alphaMax) alphaMax = alpha;
        }
        var ratio = 255 / alphaMax;
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            alpha = imageDataObject.data[i + 3];
            imageDataObject.data[i + 3] = Math.floor(alpha * ratio);
        }

        var colorSpectrum = this.createColorSpectrum();
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            var alpha = imageDataObject.data[i + 3];
            var colorR = colorSpectrum[alpha * 4];
            var colorG = colorSpectrum[alpha * 4 + 1];
            var colorB = colorSpectrum[alpha * 4 + 2];
            var colorA = colorSpectrum[alpha * 4 + 3];
            imageDataObject.data[i] = colorR;
            imageDataObject.data[i + 1] = colorG;
            imageDataObject.data[i + 2] = colorB;
            imageDataObject.data[i + 3] = colorA;
        }
        this.ctx.putImageData(imageDataObject, 0 , 0);
    }

    return HeatChart;
})();
<commit_msg>refactored<commit_after>/*------------------------------------------
HeatChart class
Generate a canvas based HeatChart. This class will generate
a canvas, so canvas does not need to be defined in html file
before using. Simply new it, specify the dom you want to append
the heat map to and provide other required arguments.
usage:
new HeatChart(targetElement, dotArray, canvasWidth, canvasHeight, gradientMode);
arguments:
    targetElement: the dom you want to append this HeatChart into;
    dotArray: data to generate HeatChart, is an array of {x:x,y:y}, can be scaled
              in percentage or in pixel, needs to be specified by 'scalemode'.
    scaleMode: 0 for percentage scale mode, other for pixel scale mode
    canvasWidth: pre-defined canvas width;
    canvasHeight: pre-defined canvas height;
    gradientMode: color mapping mode, currently choose from 0 - 5;
    transparency: canvas transparency so you can lay it on a picture;
    displayMode: 0 for heat map, 1 for gray scale map, 2 for dots
------------------------------------------*/
var HeatChart = (function() {
    //constructor
    function HeatChart(targetElement , dotArray , configuration){
        this.dotArray = dotArray;

        this.scaleMode = (configuration.scaleMode == undefined ? 0 : configuration.scaleMode);
        this.canvasWidth = ((configuration.canvasWidth == undefined || configuration.canvasWidth == 0) ? targetElement.offsetWidth : configuration.canvasWidth);
        this.canvasHeight = ((configuration.canvasHeight == undefined || configuration.canvasHeight == 0) ? targetElement.offsetHeight : configuration.canvasHeight);
        this.gradientMode = (configuration.gradientMode == undefined ? 0 : configuration.gradientMode);
        this.gradientRadius = ((configuration.gradientRadius == undefined || configuration.gradientRadius == 0) ? 32 : configuration.gradientRadius);
        this.transparency = ((configuration.transparency == undefined || configuration.transparency == 0) ? 0.5 : configuration.transparency);
        this.displayMode = (configuration.displayMode == undefined ? 0 : configuration.displayMode);

        //decide grayScaleAlpha based on number of dots
        //grayScaleAlpha is the start color of one gradient gray scale dot
        if(this.dotArray.length <= 100) this.grayScaleAlpha = 0.2;
        else if(this.dotArray.length >= 2000) this.grayScaleAlpha = 0.048;
        else this.grayScaleAlpha = 0.208 - this.dotArray.length * 0.00008;

        //generate a canvas in targetElement dom to display HeatChart
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;
        this.canvas.style.position = 'absolute';
        targetElement.appendChild(this.canvas);

        //supported color gradient mode for color mapping
        this.gradientRainbow = {
            // 0:      'rgba(0,0,0,0.0)',
            0.14:   'rgba(139,0,255,' + this.transparency + ')',
            0.28:   'rgba(0,0,255,' + this.transparency + ')',
            0.42:   'rgba(0,127,255,' + this.transparency + ')',
            0.56:   'rgba(0,255,0,' + this.transparency + ')',
            0.70:   'rgba(255,255,0,' + this.transparency + ')',
            0.84:   'rgba(255,164,0,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        }
        this.gradientWarm = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(255,255,153,' + this.transparency + ')',
            0.7:    'rgba(255,153,51,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        };
        this.gradientCold = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(102,255,102,' + this.transparency + ')',
            0.7:    'rgba(0,51,153,' + this.transparency + ')',
            1.0:    'rgba(153,0,204,' + this.transparency + ')'
        };
        this.gradientBurn = {
            0:      'rgba(0,0,0,0.0)',
            0.4:    'rgba(255,255,102,' + this.transparency + ')',
            0.7:    'rgba(255,102,0,' + this.transparency + ')',
            1.0:    'rgba(153,51,153,' + this.transparency + ')'
         };
        this.gradientLight = {
            0:      'rgba(0,0,0,0.0)',
            0.3:    'rgba(153,255,102,' + this.transparency + ')',
            0.5:    'rgba(255,255,0,' + this.transparency + ')',
            0.75:   'rgba(255,164,0,' + this.transparency + ')',
            1.0:    'rgba(255,0,0,' + this.transparency + ')'
        };

        if(this.displayMode == 2){
            this.drawDots();
        }else if (this.displayMode == 1){
            this.drawCircles();
        }else if (this.displayMode == 0){
            this.drawCircles();
            this.grayToColor();
        }
    }

    //create color spectrum canvas and return its data
    HeatChart.prototype.createColorSpectrum = function(){
        var gradientConfiguration;
        switch(this.gradientMode){
            case 0:
            gradientConfiguration = this.gradientRainbow;
            break;
            case 1:
            gradientConfiguration = this.gradientWarm;
            break;
            case 2:
            gradientConfiguration = this.gradientCold;
            break;
            case 3:
            gradientConfiguration = this.gradientBurn;
            break;
            case 4:
            gradientConfiguration = this.gradientLight;
            break;
            default:
            gradientConfiguration = this.gradientRaindow;
            break;
        }

        var spectrumCanvas = document.createElement('canvas');
        var spectrumCtx = spectrumCanvas.getContext('2d');
        spectrumCanvas.width = 256;
        spectrumCanvas.height = 1;

        var gradient = spectrumCtx.createLinearGradient(0 , 0 , 256 , 1);
        for (var colorStop in gradientConfiguration)
            gradient.addColorStop(colorStop , gradientConfiguration[colorStop]);
        spectrumCtx.fillStyle = gradient;
        spectrumCtx.fillRect(0 , 0 , 256 , 1);
        return spectrumCtx.getImageData(0 , 0 , 256 , 1).data;
    }

    //convert percentage coordinates to pixels
    HeatChart.prototype.toPixel = function(dot){
        return{
            x: dot.x / 100 * this.canvasWidth,
            y: dot.y / 100 * this.canvasHeight
        }
    }

    // draw a dot based on given coordinate, called by this.drawDots()
    HeatChart.prototype.drawOneDot = function(x , y){
        this.ctx.beginPath();
        this.ctx.arc(x , y , 3 , 0 , 2*Math.PI , false);
        this.ctx.fill();
    }

    //draw dots data as black dots on the canvas for test usage
    HeatChart.prototype.drawDots = function(){
        this.ctx.fillStyle = '#000000';
        var dotInPixel;
        for(var i = 0; i < this.dotArray.length ; i++){
            if(this.scaleMode == 0){
                dotInPixel = this.toPixel(this.dotArray[i]);
                this.drawOneDot(dotInPixel.x, dotInPixel.y);
            }else{
                this.drawOneDot(this.dotArray[i]);
            }
        }
    }

    //draw a transtargetElement radial gradient circle
    HeatChart.prototype.drawOneCircle = function(dot){
        var gradient = this.ctx.createRadialGradient(dot.x , dot.y , 1 , dot.x , dot.y , this.gradientRadius);
        this.ctx.beginPath();
        gradient.addColorStop(0 , 'rgba(0,0,0,' + this.grayScaleAlpha + ')');
        gradient.addColorStop(1.0 , 'rgba(0,0,0,0)');
        this.ctx.fillStyle = gradient;
        this.ctx.arc(dot.x , dot.y , this.gradientRadius , 2*Math.PI , false);
        this.ctx.fill();
    }

    //draw circles for each dot
    HeatChart.prototype.drawCircles = function(){
        var dotInPixel;
        for(var i = 0 ; i < this.dotArray.length ; i++){
            if(this.scaleMode == 0){
                dotInPixel = this.toPixel(this.dotArray[i]);
                this.drawOneCircle(dotInPixel);
            }else{
                this.drawOneCircle(this.dotArray[i]);
            }
        }
    }

    //convert grayscale image to color by mapping the color on colorband
    HeatChart.prototype.grayToColor = function(){
        var imageDataObject = this.ctx.getImageData(0 , 0 , this.canvasWidth , this.canvasHeight);
        var alphaMax = 0;
        var alpha;
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            alpha = imageDataObject.data[i + 3];
            if (alpha > alphaMax) alphaMax = alpha;
        }
        var ratio = 255 / alphaMax;
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            alpha = imageDataObject.data[i + 3];
            imageDataObject.data[i + 3] = Math.floor(alpha * ratio);
        }

        var colorSpectrum = this.createColorSpectrum();
        var alpha , colorR , colorG, colorB , colorA;
        for(var i = 0 ; i < imageDataObject.data.length ; i += 4){
            alpha = imageDataObject.data[i + 3];
            colorR = colorSpectrum[alpha * 4];
            colorG = colorSpectrum[alpha * 4 + 1];
            colorB = colorSpectrum[alpha * 4 + 2];
            colorA = colorSpectrum[alpha * 4 + 3];
            imageDataObject.data[i] = colorR;
            imageDataObject.data[i + 1] = colorG;
            imageDataObject.data[i + 2] = colorB;
            imageDataObject.data[i + 3] = colorA;
        }
        this.ctx.putImageData(imageDataObject, 0 , 0);
    }

    HeatChart.prototype.wipeCanvas = function(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }

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


<commit_before>var MediaObject = new JS.Class({
    initialize: function(siteName, url, permalink, id, siteMediaID, artist, mediaName, type) {
        this.id = id.toString() != "" ? id.toString() : "";
        this.siteMediaID = siteMediaID != "" ? siteMediaID.toString() : "";
        this.permalink = permalink != "" ? permalink : "";
        this.url = url;
        this.artist = artist != "" ? artist : "";
        this.siteName = siteName != "" ? siteName : "";
        this.mediaName = mediaName != "" ? mediaName : "";
        this.type = type != "" ? type : "";
    }
});

var SoundObject = new JS.Class(MediaObject, {
    initialize: function(siteName, url, permalink, id, siteMediaID, soundManager, artist, soundName, duration) {
        this.soundManager = (soundManager != undefined && soundManager != null) ? soundManager : null;
        this.duration = duration;
        this.sound = soundManager.createSound({
            id: id,
            url: url
        });
        if (this.sound == false) {
            alert("Unable to play sound.");
        }
        this.callSuper(siteName, url, permalink, id, siteMediaID, artist, soundName, "audio");
    },

    destruct: function() {
        return this.sound.destruct();
    },

    getDuration: function() {
        return this.duration;
    },

    isPaused: function() {
        return this.sound.paused;
    },

    isPlaying: function() {
        return this.sound.playState == 1;
    },

    play: function(options) {
        return this.soundManager.play(this.id, options);
    },

    seek: function(decimalPercent) {
        return this.sound.setPosition(Math.floor(decimalPercent * this.sound.duration));
    },

    stop: function() {
        return this.sound.stop();
    },

    togglePause: function() {
        return this.sound.togglePause();
    }
});

var SoundCloudObject = new JS.Class(SoundObject, {
    initialize: function(id, url, consumerKey, track, soundManager) {
        var apiURL = url;
        (apiURL.toString().indexOf("secret_token") == -1) ? apiURL = apiURL + '?' : apiURL = apiURL + '&';
        apiURL = apiURL + 'consumer_key=' + consumerKey;
        this.callSuper("SoundCloud", apiURL, track.permalink_url, id, track.id, soundManager, track.user.username, track.title, track.duration / 1000);
    }
});

var BandcampObject = new JS.Class(SoundObject, {
    initialize: function(id, linkURL, consumerKey, track, artistName, soundManager) {
        var apiURL = track.streaming_url;
        apiURL = apiURL + '&api_key=' + consumerKey;
        this.callSuper("Bandcamp", apiURL, linkURL, id, track.track_id, soundManager, artistName, track.title, track.duration);
    }
});

var VideoObject = new JS.Class(MediaObject, {
    initialize: function(siteName, url, permalink, id, siteMediaID, artist, videoName, duration) {
        this.callSuper(siteName, url, permalink, id, siteMediaID, artist, videoName, "video");
        this.duration = duration;
    }
});

var YouTubeObject = new JS.Class(VideoObject, {
    initialize: function(id, youtubeID, uploader, title, duration) {
        var permalink = 'http://www.youtube.com/watch?v=' + youtubeID;
        this.callSuper("YouTube", permalink, permalink, id, youtubeID, uploader, title, duration);
    },

    destruct: function() {
        if ($('#video').width()) {
            var videoID = $('#video').tubeplayer('videoId');
            if (videoID == this.siteMediaID) {
                body.close('east');
                $('#video').tubeplayer('destruct');
                $("#video").replaceWith('<div id="video"></id>');
            }
        }
    },

    getDuration: function() {
        return this.duration;
    },

    isPaused: function() {
        var paused;
        try {
            paused = $('#video').tubeplayer('isPaused');
        }
        catch(e) {
            paused = false;
        }
        return paused;
    },

    isPlaying: function() {
        var playing;
        try {
            playing = $('#video').tubeplayer('isPlaying');
        }
        catch(e) {
            playing = false;
        }
        return playing;
    },

    play: function(options) {
        if ($('.flashContainer').length == 0) {
            body.open('east');
        }
        $(document).ready(function() {
            if (options) {
                $("#video").tubeplayer(options);
            }
            else {
                $("#video").tubeplayer();
            }
        });
    },

    seek: function(decimalPercent) {
        var duration = this.duration;
        try {
            // Use floor function in case rounding would otherwise result in
            // a value of 101% of the total time
            $("#video").tubeplayer('seek', Math.floor(decimalPercent * duration));
        }
        catch(e) {
        // Eh, don't try anything if seeking isn't possible'
        }
    },

    stop: function() {
        $(document).ready(function() {
            $('#video').tubeplayer('stop');
        });
    },

    togglePause: function() {
        var track = this;
        $(document).ready(function() {
            if (track.isPlaying()) {
                $('#video').tubeplayer('pause');
            }
            else if (track.isPaused()) {
                $('#video').tubeplayer('play');
            }
        });
    }
});<commit_msg>Set YouTube to use the general function to clear the video panel on destruct calls.<commit_after>var MediaObject = new JS.Class({
    initialize: function(siteName, url, permalink, id, siteMediaID, artist, mediaName, type) {
        this.id = id.toString() != "" ? id.toString() : "";
        this.siteMediaID = siteMediaID != "" ? siteMediaID.toString() : "";
        this.permalink = permalink != "" ? permalink : "";
        this.url = url;
        this.artist = artist != "" ? artist : "";
        this.siteName = siteName != "" ? siteName : "";
        this.mediaName = mediaName != "" ? mediaName : "";
        this.type = type != "" ? type : "";
    }
});

var SoundObject = new JS.Class(MediaObject, {
    initialize: function(siteName, url, permalink, id, siteMediaID, soundManager, artist, soundName, duration) {
        this.soundManager = (soundManager != undefined && soundManager != null) ? soundManager : null;
        this.duration = duration;
        this.sound = soundManager.createSound({
            id: id,
            url: url
        });
        if (this.sound == false) {
            alert("Unable to play sound.");
        }
        this.callSuper(siteName, url, permalink, id, siteMediaID, artist, soundName, "audio");
    },

    destruct: function() {
        return this.sound.destruct();
    },

    getDuration: function() {
        return this.duration;
    },

    isPaused: function() {
        return this.sound.paused;
    },

    isPlaying: function() {
        return this.sound.playState == 1;
    },

    play: function(options) {
        return this.soundManager.play(this.id, options);
    },

    seek: function(decimalPercent) {
        return this.sound.setPosition(Math.floor(decimalPercent * this.sound.duration));
    },

    stop: function() {
        return this.sound.stop();
    },

    togglePause: function() {
        return this.sound.togglePause();
    }
});

var SoundCloudObject = new JS.Class(SoundObject, {
    initialize: function(id, url, consumerKey, track, soundManager) {
        var apiURL = url;
        (apiURL.toString().indexOf("secret_token") == -1) ? apiURL = apiURL + '?' : apiURL = apiURL + '&';
        apiURL = apiURL + 'consumer_key=' + consumerKey;
        this.callSuper("SoundCloud", apiURL, track.permalink_url, id, track.id, soundManager, track.user.username, track.title, track.duration / 1000);
    }
});

var BandcampObject = new JS.Class(SoundObject, {
    initialize: function(id, linkURL, consumerKey, track, artistName, soundManager) {
        var apiURL = track.streaming_url;
        apiURL = apiURL + '&api_key=' + consumerKey;
        this.callSuper("Bandcamp", apiURL, linkURL, id, track.track_id, soundManager, artistName, track.title, track.duration);
    }
});

var VideoObject = new JS.Class(MediaObject, {
    initialize: function(siteName, url, permalink, id, siteMediaID, artist, videoName, duration) {
        this.callSuper(siteName, url, permalink, id, siteMediaID, artist, videoName, "video");
        this.duration = duration;
    }
});

var YouTubeObject = new JS.Class(VideoObject, {
    initialize: function(id, youtubeID, uploader, title, duration) {
        var permalink = 'http://www.youtube.com/watch?v=' + youtubeID;
        this.callSuper("YouTube", permalink, permalink, id, youtubeID, uploader, title, duration);
    },

    destruct: function() {
        if ($('#video').width()) {
            var videoID = $('#video').tubeplayer('videoId');
            if (videoID == this.siteMediaID) {
                clearVideo();
            }
        }
    },

    getDuration: function() {
        return this.duration;
    },

    isPaused: function() {
        var paused;
        try {
            paused = $('#video').tubeplayer('isPaused');
        }
        catch(e) {
            paused = false;
        }
        return paused;
    },

    isPlaying: function() {
        var playing;
        try {
            playing = $('#video').tubeplayer('isPlaying');
        }
        catch(e) {
            playing = false;
        }
        return playing;
    },

    play: function(options) {
        if ($('.flashContainer').length == 0) {
            body.open('east');
        }
        $(document).ready(function() {
            if (options) {
                $("#video").tubeplayer(options);
            }
            else {
                $("#video").tubeplayer();
            }
        });
    },

    seek: function(decimalPercent) {
        var duration = this.duration;
        try {
            // Use floor function in case rounding would otherwise result in
            // a value of 101% of the total time
            $("#video").tubeplayer('seek', Math.floor(decimalPercent * duration));
        }
        catch(e) {
        // Eh, don't try anything if seeking isn't possible'
        }
    },

    stop: function() {
        $(document).ready(function() {
            $('#video').tubeplayer('stop');
        });
    },

    togglePause: function() {
        var track = this;
        $(document).ready(function() {
            if (track.isPlaying()) {
                $('#video').tubeplayer('pause');
            }
            else if (track.isPaused()) {
                $('#video').tubeplayer('play');
            }
        });
    }
});<|endoftext|>


<commit_before>// Copyright (c) 2009, Scott Ferguson
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the software nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL SCOTT FERGUSON BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

function HotKeyManager() {
    /*****************
     N - Next Post
     P/M - Previous Post
     J - Next Page
     K/H - Previous Page
     O - Reanchor thread
     Q - Quick Quote current post
     E - Quick Edit current post
     R - Quick Reply current thread
    *******************/
    this.bindHotKeys();
    this.current_post = this.findFirstUnreadPost();

    this.thread_post_size = jQuery('div#thread > table.post').size();
}

HotKeyManager.prototype.bindHotKeys = function() {
    var that = this;

    jQuery(document).keypress(function(event) {
        var quick_reply_block = false;

        if (findCurrentPage() == 'showthread.php') {
            if (quickReply.isExpanded() || quickReply.isVisible()) {
                quick_reply_block = true;
            }
        }

        if (!quick_reply_block) {
            switch(event.keyCode) {
                case 110:
                    // Next post
                    that.nextPost();
                    break;
                case 112:
                case 109:
                    // Previous post
                    that.previousPost();
                    break;
                case 106:
                    // Next page
                    that.nextPage();
                    break;
                case 107:
                case 104:
                    // Previous page
                    that.previousPage();
                    break;
                case 111:
                    // Re-anchor thread
                    that.anchorThread();
                    break;
                case 113:
                    // Quick quote current post
                    that.quoteCurrentPost();
                    break;
                case 101:
                    // Quick edit current post
                    break;
                case 114:
                    // TODO: Conditionalize on quick reply being enabled
                    that.displayQuickReply();
                    break;
                case 27:
                    // TODO: Conditionalize on quick reply being enabled
                    that.hideQuickReply();
                    break;
            }
        }
    });
};

HotKeyManager.prototype.findFirstUnreadPost = function() {
    var index = 0;
    var count = 0;

    jQuery('div#thread > table.post').each(function() {
        var post = jQuery('tr', this);

        if (post.hasClass('seen1') || post.hasClass('seen2')) {
            index = count;
        }

        count++;
    });

    return index;
};

HotKeyManager.prototype.nextPage = function() {
    this.pageCount = countPages();

    switch(findCurrentPage()) {
        case 'forumdisplay.php':
        case 'showthread.php':
            this.rootPageType = (findCurrentPage() == 'forumdisplay.php') ? 'forumid' : 'threadid';
            this.basePageID = findForumID();
            this.currentPage = Number(jQuery('span.curpage').html());
            if (this.currentPage <= 0)
                this.currentPage = 1;
            if (this.currentPage < this.pageCount)
                jumpToPage(this.rootPageType, this.basePageID, this.currentPage + 1);
            break;
    }
};

HotKeyManager.prototype.previousPage = function() {
    this.pageCount = countPages();

    switch(findCurrentPage()) {
        case 'forumdisplay.php':
        case 'showthread.php':
            this.rootPageType = (findCurrentPage() == 'forumdisplay.php') ? 'forumid' : 'threadid';
            this.basePageID = findForumID();
            this.currentPage = Number(jQuery('span.curpage').html());
            if (this.currentPage <= 0)
                this.currentPage = 1;
            if (this.currentPage > 1)
                jumpToPage(this.rootPageType, this.basePageID, this.currentPage - 1);
            break;
    }
};

HotKeyManager.prototype.nextPost = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    }

    if (this.current_post < (this.thread_post_size - 1)) {
        this.current_post++;
    }

    var post = jQuery('div#thread > table.post');
    var previous_post = post.eq(this.current_post - 1);
    var current_post = post.eq(this.current_post);
    previous_post.css('border', '1px solid #c1c1c1');
    current_post.css('border', '2px dashed #aaa');

    jQuery(window).scrollTop(current_post.offset().top);
};

HotKeyManager.prototype.previousPost = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    }

    if (this.current_post > 0) {
        this.current_post--;
    }

    var post = jQuery('div#thread > table.post');
    var previous_post = post.eq(this.current_post + 1);
    var current_post = post.eq(this.current_post);
    previous_post.css('border', '1px solid #c1c1c1');
    current_post.css('border', '2px dashed #aaa');

    jQuery(window).scrollTop(current_post.offset().top);
};

HotKeyManager.prototype.anchorThread = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    } else if (this.current_post == -1) {
        return;
    }

    var post = jQuery('div#thread > table.post').eq(this.current_post);

    jQuery(window).scrollTop(post.offset().top);
};

HotKeyManager.prototype.quoteCurrentPost = function() {
    if (this.current_post == -1) {
        return;
    }

    var current_post = jQuery('div#thread > table.post').eq(this.current_post);
    var username = jQuery('tr > td.userinfo > dl > dt.author', current_post).html();
    // Query for the quote
    var quote = jQuery('tr > td.postbody', current_post).clone();

    quickReply.appendQuote(username, quote);
    quickReply.show();
};

HotKeyManager.prototype.displayQuickReply = function() {
    if (findCurrentPage() == 'showthread.php') {
        quickReply.show();
    }
};

HotKeyManager.prototype.hideQuickReply = function() {
    if (findCurrentPage() == 'showthread.php') {
        quickReply.hide();
    }
};
<commit_msg>Find first unread post on a page by 2 possible methods: setseen or post coloring Fixed off-by-one error when finding first unread post Set border on selected post when reanchoring<commit_after>// Copyright (c) 2009, Scott Ferguson
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the software nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL SCOTT FERGUSON BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

function HotKeyManager() {
    /*****************
     N - Next Post
     P/M - Previous Post
     J - Next Page
     K/H - Previous Page
     O - Reanchor thread
     Q - Quick Quote current post
     E - Quick Edit current post
     R - Quick Reply current thread
    *******************/
    this.bindHotKeys();

    this.thread_post_size = jQuery('div#thread > table.post').size();
    this.current_post = this.findFirstUnreadPost();
}

HotKeyManager.prototype.bindHotKeys = function() {
    var that = this;

    jQuery(document).keypress(function(event) {
        var quick_reply_block = false;

        if (findCurrentPage() == 'showthread.php') {
            if (quickReply.isExpanded() || quickReply.isVisible()) {
                quick_reply_block = true;
            }
        }

        if (!quick_reply_block) {
            switch(event.keyCode) {
                case 110:
                    // Next post
                    that.nextPost();
                    break;
                case 112:
                case 109:
                    // Previous post
                    that.previousPost();
                    break;
                case 106:
                    // Next page
                    that.nextPage();
                    break;
                case 107:
                case 104:
                    // Previous page
                    that.previousPage();
                    break;
                case 111:
                    // Re-anchor thread
                    that.anchorThread();
                    break;
                case 113:
                    // Quick quote current post
                    that.quoteCurrentPost();
                    break;
                case 101:
                    // Quick edit current post
                    break;
                case 114:
                    // TODO: Conditionalize on quick reply being enabled
                    that.displayQuickReply();
                    break;
                case 27:
                    // TODO: Conditionalize on quick reply being enabled
                    that.hideQuickReply();
                    break;
            }
        }
    });
};

HotKeyManager.prototype.findFirstUnreadPost = function() {
    var index = 0;
    var count = 0;

    // Check if the user has the "Show an icon next to each post indicating if
    // it has been seen or not" option enabled for the forums
    var use_setseen = (jQuery('td.postdate > a[href*=action=setseen]').length > 0);

    jQuery('div#thread > table.post').each(function() {
        if (use_setseen) {
            // User has setseen icons enabled
            var posticon_img = jQuery('img[src*=posticon]', this);

            count++;
            if (posticon_img.attr('src') == 'http://fi.somethingawful.com/style/posticon-seen.gif') {
                index = count;
            }
        } else {
            // User has read post coloring enabled
            var post = jQuery('tr', this);

            count++;
            if (post.hasClass('seen1') || post.hasClass('seen2')) {
                index = count;
            }
        }
    });

    if (index == this.thread_post_size) // All posts read
        return index-1;

    return index;
};

HotKeyManager.prototype.nextPage = function() {
    this.pageCount = countPages();

    switch(findCurrentPage()) {
        case 'forumdisplay.php':
        case 'showthread.php':
            this.rootPageType = (findCurrentPage() == 'forumdisplay.php') ? 'forumid' : 'threadid';
            this.basePageID = findForumID();
            this.currentPage = Number(jQuery('span.curpage').html());
            if (this.currentPage <= 0)
                this.currentPage = 1;
            if (this.currentPage < this.pageCount)
                jumpToPage(this.rootPageType, this.basePageID, this.currentPage + 1);
            break;
    }
};

HotKeyManager.prototype.previousPage = function() {
    this.pageCount = countPages();

    switch(findCurrentPage()) {
        case 'forumdisplay.php':
        case 'showthread.php':
            this.rootPageType = (findCurrentPage() == 'forumdisplay.php') ? 'forumid' : 'threadid';
            this.basePageID = findForumID();
            this.currentPage = Number(jQuery('span.curpage').html());
            if (this.currentPage <= 0)
                this.currentPage = 1;
            if (this.currentPage > 1)
                jumpToPage(this.rootPageType, this.basePageID, this.currentPage - 1);
            break;
    }
};

HotKeyManager.prototype.nextPost = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    }

    if (this.current_post < (this.thread_post_size - 1)) {
        this.current_post++;
    }

    var post = jQuery('div#thread > table.post');
    var previous_post = post.eq(this.current_post - 1);
    var current_post = post.eq(this.current_post);
    previous_post.css('border', '1px solid #c1c1c1');
    current_post.css('border', '2px dashed #aaa');

    jQuery(window).scrollTop(current_post.offset().top);
};

HotKeyManager.prototype.previousPost = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    }

    if (this.current_post > 0) {
        this.current_post--;
    }

    var post = jQuery('div#thread > table.post');
    var previous_post = post.eq(this.current_post + 1);
    var current_post = post.eq(this.current_post);
    previous_post.css('border', '1px solid #c1c1c1');
    current_post.css('border', '2px dashed #aaa');

    jQuery(window).scrollTop(current_post.offset().top);
};

HotKeyManager.prototype.anchorThread = function() {
    if (!findCurrentPage() == 'showthread.php') {
        return;
    } else if (this.current_post == -1) {
        return;
    }

    var post = jQuery('div#thread > table.post').eq(this.current_post);
    post.css('border', '2px dashed #aaa');

    jQuery(window).scrollTop(post.offset().top);
};

HotKeyManager.prototype.quoteCurrentPost = function() {
    if (this.current_post == -1) {
        return;
    }

    var current_post = jQuery('div#thread > table.post').eq(this.current_post);
    var username = jQuery('tr > td.userinfo > dl > dt.author', current_post).html();
    // Query for the quote
    var quote = jQuery('tr > td.postbody', current_post).clone();

    quickReply.appendQuote(username, quote);
    quickReply.show();
};

HotKeyManager.prototype.displayQuickReply = function() {
    if (findCurrentPage() == 'showthread.php') {
        quickReply.show();
    }
};

HotKeyManager.prototype.hideQuickReply = function() {
    if (findCurrentPage() == 'showthread.php') {
        quickReply.hide();
    }
};
<|endoftext|>


<commit_before>/* @flow strict-local */
import union from 'lodash.union';

import type { NarrowsState, Action } from '../types';
import {
  DEAD_QUEUE,
  LOGOUT,
  LOGIN_SUCCESS,
  ACCOUNT_SWITCH,
  MESSAGE_FETCH_COMPLETE,
  EVENT_NEW_MESSAGE,
  EVENT_MESSAGE_DELETE,
  EVENT_UPDATE_MESSAGE_FLAGS,
} from '../actionConstants';
import { LAST_MESSAGE_ANCHOR, FIRST_UNREAD_ANCHOR } from '../constants';
import { isMessageInNarrow, MENTIONED_NARROW_STR, STARRED_NARROW_STR } from '../utils/narrow';
import { NULL_OBJECT } from '../nullObjects';

const initialState: NarrowsState = NULL_OBJECT;

const messageFetchComplete = (state, action) => {
  const key = JSON.stringify(action.narrow);
  const fetchedMessageIds = action.messages.map(message => message.id);
  const replaceExisting =
    action.anchor === FIRST_UNREAD_ANCHOR || action.anchor === LAST_MESSAGE_ANCHOR;
  return {
    ...state,
    [key]: replaceExisting
      ? fetchedMessageIds
      : union(state[key], fetchedMessageIds).sort((a, b) => a - b),
  };
};

const eventNewMessage = (state, action) => {
  let stateChange = false;
  const newState: NarrowsState = {};
  Object.keys(state).forEach(key => {
    const isInNarrow = isMessageInNarrow(action.message, JSON.parse(key), action.ownEmail);
    const isCaughtUp = action.caughtUp[key] && action.caughtUp[key].newer;
    const messageDoesNotExist = state[key].find(id => action.message.id === id) === undefined;

    if (isInNarrow && isCaughtUp && messageDoesNotExist) {
      stateChange = true;
      newState[key] = [...state[key], action.message.id];
    } else {
      newState[key] = state[key];
    }
  });
  return stateChange ? newState : state;
};

const eventMessageDelete = (state, action) => {
  let stateChange = false;
  const newState: NarrowsState = {};
  Object.keys(state).forEach(key => {
    newState[key] = state[key].filter(id => id !== action.messageId);
    stateChange = stateChange || newState[key].length < state[key].length;
  });
  return stateChange ? newState : state;
};

const eventUpdateMessageFlags = (state, action) => {
  const { messages, flag, operation } = action;
  const messagesSet = new Set(messages);
  const updates = [];

  const updateFlagNarrow = narrowStr => {
    if (state[narrowStr]) {
      if (operation === 'add') {
        updates.push({
          [narrowStr]: [...state[narrowStr], ...messages].sort(),
        });
      } else if (operation === 'remove') {
        updates.push({
          [narrowStr]: state[narrowStr].filter(id => !messagesSet.has(id)),
        });
      } else {
        throw new Error(
          `Unexpected operation ${operation} in an EVENT_UPDATE_MESSAGE_FLAGS action`,
        );
      }
    }

    return updates;
  };

  if (flag === 'starred') {
    updateFlagNarrow(STARRED_NARROW_STR);
  } else if (['mentioned', 'wildcard_mentioned'].includes(flag)) {
    updateFlagNarrow(MENTIONED_NARROW_STR);
  }

  if (!updates.length) {
    return state;
  }

  return Object.assign({}, state, ...updates);
};

export default (state: NarrowsState = initialState, action: Action): NarrowsState => {
  switch (action.type) {
    case DEAD_QUEUE:
    case LOGOUT:
    case LOGIN_SUCCESS:
    case ACCOUNT_SWITCH:
      return initialState;

    case MESSAGE_FETCH_COMPLETE:
      return messageFetchComplete(state, action);

    case EVENT_NEW_MESSAGE:
      return eventNewMessage(state, action);

    case EVENT_MESSAGE_DELETE:
      return eventMessageDelete(state, action);

    case EVENT_UPDATE_MESSAGE_FLAGS:
      return eventUpdateMessageFlags(state, action);

    default:
      return state;
  }
};
<commit_msg>narrowsReducer: Check `operation` match is exhaustive statically, too.<commit_after>/* @flow strict-local */
import union from 'lodash.union';

import type { NarrowsState, Action } from '../types';
import { ensureUnreachable } from '../types';
import {
  DEAD_QUEUE,
  LOGOUT,
  LOGIN_SUCCESS,
  ACCOUNT_SWITCH,
  MESSAGE_FETCH_COMPLETE,
  EVENT_NEW_MESSAGE,
  EVENT_MESSAGE_DELETE,
  EVENT_UPDATE_MESSAGE_FLAGS,
} from '../actionConstants';
import { LAST_MESSAGE_ANCHOR, FIRST_UNREAD_ANCHOR } from '../constants';
import { isMessageInNarrow, MENTIONED_NARROW_STR, STARRED_NARROW_STR } from '../utils/narrow';
import { NULL_OBJECT } from '../nullObjects';

const initialState: NarrowsState = NULL_OBJECT;

const messageFetchComplete = (state, action) => {
  const key = JSON.stringify(action.narrow);
  const fetchedMessageIds = action.messages.map(message => message.id);
  const replaceExisting =
    action.anchor === FIRST_UNREAD_ANCHOR || action.anchor === LAST_MESSAGE_ANCHOR;
  return {
    ...state,
    [key]: replaceExisting
      ? fetchedMessageIds
      : union(state[key], fetchedMessageIds).sort((a, b) => a - b),
  };
};

const eventNewMessage = (state, action) => {
  let stateChange = false;
  const newState: NarrowsState = {};
  Object.keys(state).forEach(key => {
    const isInNarrow = isMessageInNarrow(action.message, JSON.parse(key), action.ownEmail);
    const isCaughtUp = action.caughtUp[key] && action.caughtUp[key].newer;
    const messageDoesNotExist = state[key].find(id => action.message.id === id) === undefined;

    if (isInNarrow && isCaughtUp && messageDoesNotExist) {
      stateChange = true;
      newState[key] = [...state[key], action.message.id];
    } else {
      newState[key] = state[key];
    }
  });
  return stateChange ? newState : state;
};

const eventMessageDelete = (state, action) => {
  let stateChange = false;
  const newState: NarrowsState = {};
  Object.keys(state).forEach(key => {
    newState[key] = state[key].filter(id => id !== action.messageId);
    stateChange = stateChange || newState[key].length < state[key].length;
  });
  return stateChange ? newState : state;
};

const eventUpdateMessageFlags = (state, action) => {
  const { messages, flag, operation } = action;
  const messagesSet = new Set(messages);
  const updates = [];

  const updateFlagNarrow = narrowStr => {
    if (state[narrowStr]) {
      if (operation === 'add') {
        updates.push({
          [narrowStr]: [...state[narrowStr], ...messages].sort(),
        });
      } else if (operation === 'remove') {
        updates.push({
          [narrowStr]: state[narrowStr].filter(id => !messagesSet.has(id)),
        });
      } else {
        ensureUnreachable(operation);
        throw new Error(
          `Unexpected operation ${operation} in an EVENT_UPDATE_MESSAGE_FLAGS action`,
        );
      }
    }

    return updates;
  };

  if (flag === 'starred') {
    updateFlagNarrow(STARRED_NARROW_STR);
  } else if (['mentioned', 'wildcard_mentioned'].includes(flag)) {
    updateFlagNarrow(MENTIONED_NARROW_STR);
  }

  if (!updates.length) {
    return state;
  }

  return Object.assign({}, state, ...updates);
};

export default (state: NarrowsState = initialState, action: Action): NarrowsState => {
  switch (action.type) {
    case DEAD_QUEUE:
    case LOGOUT:
    case LOGIN_SUCCESS:
    case ACCOUNT_SWITCH:
      return initialState;

    case MESSAGE_FETCH_COMPLETE:
      return messageFetchComplete(state, action);

    case EVENT_NEW_MESSAGE:
      return eventNewMessage(state, action);

    case EVENT_MESSAGE_DELETE:
      return eventMessageDelete(state, action);

    case EVENT_UPDATE_MESSAGE_FLAGS:
      return eventUpdateMessageFlags(state, action);

    default:
      return state;
  }
};
<|endoftext|>


<commit_before>import {spawn} from 'child_process';
import {ipcRenderer} from 'electron';

import React from 'react';
import Loader from 'react-loader';

import VersionStore from './../stores/version-store';
import VersionActions from './../actions/version-actions';

import remote from 'remote';

import AddTags from './add-tags';
import PictureInfo from './picture-info';

var Menu = remote.require('menu');
var MenuItem = remote.require('menu-item');

var rotation = {};
rotation[1] = '';
rotation[0] = 'minus-ninety';

class PictureDetail extends React.Component {

  constructor(props) {
    super(props);

    this.state = { binded: false, modalIsOpen: false, loaded: false };

    this.keyboardListener = this.keyboardListener.bind(this);
    this.contextMenu = this.contextMenu.bind(this);
    this.bindEventListeners = this.bindEventListeners.bind(this);
    this.unbindEventListeners = this.unbindEventListeners.bind(this);
    this.closeTagDialog = this.closeTagDialog.bind(this);
    this.finishLoading = this.finishLoading.bind(this);
  }

  updateVersion(store) {
    if (store.version) this.setState(store);
  }

  keyboardListener(e) {
    e.preventDefault();

    if ([27, 37, 39, 80].indexOf(e.keyCode) != -1)
      this.unbindEventListeners();

    if (e.keyCode == 27 && !this.state.modalIsOpen) // escape
      this.props.setCurrent(null);

    else if (e.keyCode == 27 && this.state.modalIsOpen) // escape
      this.closeTagDialog();

    else if (e.keyCode == 37) // Left
      this.props.setLeft();

    else if (e.keyCode == 39) // Right
      this.props.setRight();

    else if (e.keyCode == 80) // p
      this.props.toggleFlag();

    else if (e.keyCode == 89 && this.props.photo.versionNumber > 1) // y
      this.props.showDiff();

    else if (this.props.isLast() && !this.state.binded)
      this.bindEventListeners();
  }

  contextMenu(e) {
    e.preventDefault();
    this.menu.popup(remote.getCurrentWindow());
  }

  openWithRawtherapee() {
    VersionActions.createVersionAndOpenWith(
      this.props.photo,
      'RAW',
      'rawtherapee'
    );
  }

  openWithGimp() {
    VersionActions.createVersionAndOpenWith(this.props.photo, 'JPG', 'gimp');
  }

  addRawtherapeeMenu() {
    this.menu.append(new MenuItem({
      label: 'Open with Rawtherapee',
      click: this.openWithRawtherapee.bind(this)
    }));
  }

  addGimpMenu() {
    this.menu.append(new MenuItem({
      label: 'Open with Gimp',
      click: this.openWithGimp.bind(this)
    }));
  }

  showTagDialog() {
    this.unbindEventListeners();

    var state = this.state;
    state.modalIsOpen = true;
    this.setState(state);
  }

  closeTagDialog() {
    this.bindEventListeners();

    var state = this.state;
    state.modalIsOpen = false;
    this.setState(state);
  }

  componentDidMount() {
    //console.log('start loader');
    VersionStore.listen(this.updateVersion.bind(this));

    this.menu = new Menu();

    this.menu.append(new MenuItem({
      label: 'Add tag',
      click: this.showTagDialog.bind(this)
    }));

    this.menu.append(new MenuItem({
      type: 'separator'
    }));

    let rawtherapeeCmd = spawn('which', ['rawtherapee']);
    let gimpCmd = spawn('which', ['gimp']);

    rawtherapeeCmd.stdout.on('data', this.addRawtherapeeMenu.bind(this));
    gimpCmd.stdout.on('data', this.addGimpMenu.bind(this));

    this.bindEventListeners();
  }

  finishLoading() {
    //console.log('stop loader');
    let state = this.state;

    state.loaded = true;

    this.setState(state);
  }

  componentWillReceiveProps() {
    this.bindEventListeners();
  }

  componentWillUnmount() {
    this.unbindEventListeners();

    delete this.menu;
  }

  bindEventListeners() {
    let state = this.state;
    state.binded = true;
    this.setState(state);

    document.addEventListener('keyup', this.keyboardListener);
    document.addEventListener('contextmenu', this.contextMenu);

    ipcRenderer.send('toggleAddTagMenu', true);
    ipcRenderer.on('addTagClicked', this.showTagDialog.bind(this));
  }

  unbindEventListeners() {
    let state = this.state;
    state.binded = false;
    this.setState(state);

    document.removeEventListener('keyup', this.keyboardListener);
    document.removeEventListener('contextmenu', this.contextMenu);

    ipcRenderer.send('toggleAddTagMenu', false);
    ipcRenderer.removeListener('addTagClicked', this.showTagDialog.bind(this));
    ipcRenderer.removeAllListeners('addTagClicked');
  }

  render() {
    var className = [
      'shadow--2dp',
      rotation[this.props.photo.orientation]
    ].join(' ');

    var showModal;

    if (this.state.modalIsOpen)
      showModal = (
        <AddTags
          photo={this.props.photo}
          closeTagDialog={this.closeTagDialog} />
      );

    return (

        <PictureInfo photo={this.props.photo} />
        <Loader loaded={this.state.loaded} />

        {showModal}

    );
  }
}

export default PictureDetail;
<commit_msg>Solve deactivated keyboard listener when reaching the last image<commit_after>import {spawn} from 'child_process';
import {ipcRenderer} from 'electron';

import React from 'react';
import Loader from 'react-loader';

import VersionStore from './../stores/version-store';
import VersionActions from './../actions/version-actions';

import remote from 'remote';

import AddTags from './add-tags';
import PictureInfo from './picture-info';

var Menu = remote.require('menu');
var MenuItem = remote.require('menu-item');

var rotation = {};
rotation[1] = '';
rotation[0] = 'minus-ninety';

class PictureDetail extends React.Component {

  constructor(props) {
    super(props);

    this.state = { binded: false, modalIsOpen: false, loaded: false };

    this.keyboardListener = this.keyboardListener.bind(this);
    this.contextMenu = this.contextMenu.bind(this);
    this.bindEventListeners = this.bindEventListeners.bind(this);
    this.unbindEventListeners = this.unbindEventListeners.bind(this);
    this.closeTagDialog = this.closeTagDialog.bind(this);
    this.finishLoading = this.finishLoading.bind(this);
  }

  updateVersion(store) {
    if (store.version) this.setState(store);
  }

  keyboardListener(e) {
    e.preventDefault();

    if ([27, 37, 39, 80].indexOf(e.keyCode) != -1)
      this.unbindEventListeners();

    if (e.keyCode == 27 && !this.state.modalIsOpen) // escape
      this.props.setCurrent(null);

    else if (e.keyCode == 27 && this.state.modalIsOpen) // escape
      this.closeTagDialog();

    else if (e.keyCode == 37) // Left
      this.props.setLeft();

    else if (e.keyCode == 39 && !this.props.isLast()) // Right
      this.props.setRight();

    else if (e.keyCode == 80) // p
      this.props.toggleFlag();

    else if (e.keyCode == 89 && this.props.photo.versionNumber > 1) // y
      this.props.showDiff();

    else if (this.props.isLast() && !this.state.binded)
      this.bindEventListeners();
  }

  contextMenu(e) {
    e.preventDefault();
    this.menu.popup(remote.getCurrentWindow());
  }

  openWithRawtherapee() {
    VersionActions.createVersionAndOpenWith(
      this.props.photo,
      'RAW',
      'rawtherapee'
    );
  }

  openWithGimp() {
    VersionActions.createVersionAndOpenWith(this.props.photo, 'JPG', 'gimp');
  }

  addRawtherapeeMenu() {
    this.menu.append(new MenuItem({
      label: 'Open with Rawtherapee',
      click: this.openWithRawtherapee.bind(this)
    }));
  }

  addGimpMenu() {
    this.menu.append(new MenuItem({
      label: 'Open with Gimp',
      click: this.openWithGimp.bind(this)
    }));
  }

  showTagDialog() {
    this.unbindEventListeners();

    var state = this.state;
    state.modalIsOpen = true;
    this.setState(state);
  }

  closeTagDialog() {
    this.bindEventListeners();

    var state = this.state;
    state.modalIsOpen = false;
    this.setState(state);
  }

  componentDidMount() {
    //console.log('start loader');
    VersionStore.listen(this.updateVersion.bind(this));

    this.menu = new Menu();

    this.menu.append(new MenuItem({
      label: 'Add tag',
      click: this.showTagDialog.bind(this)
    }));

    this.menu.append(new MenuItem({
      type: 'separator'
    }));

    let rawtherapeeCmd = spawn('which', ['rawtherapee']);
    let gimpCmd = spawn('which', ['gimp']);

    rawtherapeeCmd.stdout.on('data', this.addRawtherapeeMenu.bind(this));
    gimpCmd.stdout.on('data', this.addGimpMenu.bind(this));

    this.bindEventListeners();
  }

  finishLoading() {
    //console.log('stop loader');
    let state = this.state;

    state.loaded = true;

    this.setState(state);
  }

  componentWillReceiveProps() {
    this.bindEventListeners();
  }

  componentWillUnmount() {
    this.unbindEventListeners();

    delete this.menu;
  }

  bindEventListeners() {
    let state = this.state;
    state.binded = true;
    this.setState(state);

    document.addEventListener('keyup', this.keyboardListener);
    document.addEventListener('contextmenu', this.contextMenu);

    ipcRenderer.send('toggleAddTagMenu', true);
    ipcRenderer.on('addTagClicked', this.showTagDialog.bind(this));
  }

  unbindEventListeners() {
    let state = this.state;
    state.binded = false;
    this.setState(state);

    document.removeEventListener('keyup', this.keyboardListener);
    document.removeEventListener('contextmenu', this.contextMenu);

    ipcRenderer.send('toggleAddTagMenu', false);
    ipcRenderer.removeListener('addTagClicked', this.showTagDialog.bind(this));
    ipcRenderer.removeAllListeners('addTagClicked');
  }

  render() {
    var className = [
      'shadow--2dp',
      rotation[this.props.photo.orientation]
    ].join(' ');

    var showModal;

    if (this.state.modalIsOpen)
      showModal = (
        <AddTags
          photo={this.props.photo}
          closeTagDialog={this.closeTagDialog} />
      );

    return (

        <PictureInfo photo={this.props.photo} />
        <Loader loaded={this.state.loaded} />

        {showModal}

    );
  }
}

export default PictureDetail;
<|endoftext|>