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

import React from 'react';
import { shallow, mount } from 'enzyme';
import chai from 'chai';
import sinon from 'sinon';
import NumericInput from './NumericInput';

const expect = chai.expect;

// Tests to write:
// new props
// validation message
// remove formatting on focus
// format on blur

/* *****************************************************************************
on blur, the NumericInput component
    should format a numeric value as a plain number, if isCurrency=false and decimals=null
    should format a numeric value as a number with n decimal places, if isCurrency=false and decimals=n
    should format a numeric values as a $#.## if isCurrency=true
*/

/* *****************************************************************************
the onChange handler for the NumericInput component
    should not be called on initialization
    should be called on edit when the value has changed
    should not be called on edit when the value has not changed
    should be called on blur when the value has changed
    should not be called on blur when the value has not changed
*/
describe('the onChange handler for the NumericInput component', () => {

    it('should not be called on initialization', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;

        mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);
    });

    it('should be called on edit when the value has changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = 99.9999;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(1);
        expect(onChange.calledWith(finalValue)).to.equal(true);
    });

    it('should not be called on edit when the value has not changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = initialValue;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(0);
    });

    it('should be called on blur when the value has changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = 4213.21;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(1);
        expect(onChange.calledWith(finalValue)).to.equal(true);
    });

    it('should not be called on blur when the value has not changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = initialValue;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(0);
    });
});

/* *****************************************************************************
the onValidation handler for the NumericInput component
    should be called on initialization with required=true, value=valid
    should be called on initialization with required=true, value=invalid
    should be called on initialization with required=true, value=blank
    should be called on initialization with required=false, value=valid
    should be called on initialization with required=false, value=invalid
    should be called on initialization with required=false, value=blank
    should not be called after change without prior blur event
    should be called on blur event
    should be called after change following a previous blur event
*/
describe('the onValidation handler for the NumericInput component', () => {

    const description = 'a numeric input';
    const expectedMessage = `${description} is required`;

    it('should be called on initialization with required=true, value=valid', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = 123.456;

        mount(
            <NumericInput
                required={required}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=true, value=invalid', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = 'this is not a number';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(false, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(expectedMessage, 'args[0][2]');
    });

    it('should be called on initialization with required=true, value=blank', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(false, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(expectedMessage, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=valid', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '101.9';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=invalid', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = 'woohoo!';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=blank', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should not be called after change without prior blur event', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '';
        const newValue = '33.3';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(1);
    });

    it('should be called on blur event', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';
        const newValue = '33.3';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(2);
        expect(onValidation.args[1][0]).to.equal(true, 'args[1][0]');
        expect(onValidation.args[1][1]).to.equal(true, 'args[1][1]');
        expect(onValidation.args[1][2]).to.equal(null, 'args[1][2]');
    });

    it('should be called after change following a previous blur event', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';
        const newValue = '33.3';
        const finalValue = '';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(2);

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

        expect(onValidation.callCount).to.equal(3);

        expect(onValidation.args[2][0]).to.equal(true, 'args[2][0]');
        expect(onValidation.args[2][1]).to.equal(false, 'args[2][1]');
        expect(onValidation.args[2][2]).to.equal(expectedMessage, 'args[2][2]');
    });

});

/* *****************************************************************************
in terms of basic markup, the NumericInput component
    should be a div.form-group
    should include an input[type="text"]
    should include an input.form-control
    should include a label with the appropriate text if a label is specified
    should include a placeholder if a placeholder is specified
    should include a label with the the required flag if a label is specified & required is set
    should not include a label with the the required flag if a label is
        specified & required is not set
    should not include a label with the the required flag if a label is not
        specified & required is set
*/
describe('in terms of basic markup, the NumericInput component', () => {

    it('should be a div.form-group', () => {
        const component = shallow(<NumericInput />);

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

    it('should include an input[type="text"]', () => {
        const component = shallow(<NumericInput />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input[type="text"]').length).to.equal(1);
    });

    it('should include an input.form-control', () => {
        const component = shallow(<NumericInput />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input.form-control').length).to.equal(1);
    });

    it('should include a label with the appropriate text if a label is specified', () => {
        const label = 'some label';

        const component = shallow(<NumericInput label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().label).to.equal(label);
    });

    it('should include a placeholder if a placeholder is specified', () => {
        const placeholder = 'a number goes here';

        const component = shallow(<NumericInput placeholder={placeholder} />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input').props().placeholder).to.equal(placeholder);
    });

    it('should include a label with the the required flag if a label is ' +
        'specified & required is set', () => {
        const label = 'some label';
        const required = true;

        const component = shallow(<NumericInput required={required} label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().required).to.equal(required);
    });

    it('should not include a label with the the required flag if a label is ' +
        'specified & required is not set', () => {
        const label = 'some label';
        const required = false;

        const component = shallow(<NumericInput required={required} label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().required).to.equal(required);
    });

    it('should not include a label with the the required flag if a label is ' +
        'not specified & required is set', () => {
        const required = false;

        const component = shallow(<NumericInput required={required} />);

        expect(component.find('Label').length).to.equal(0);
    });
});
<commit_msg>updated specs<commit_after>// the eslint "no-unused-expressions" rule is triggered by the `expect`
// assertions in the tests. So, in order to avoid problems with eslint, I'm
// disabling that rule in this file
//
/* eslint-disable no-unused-expressions */

import React from 'react';
import { shallow, mount } from 'enzyme';
import chai from 'chai';
import sinon from 'sinon';
import NumericInput from './NumericInput';

const expect = chai.expect;

// Tests to write:
// new props
// validation message
// remove formatting on focus
// format on blur

/* *****************************************************************************
on blur, the NumericInput component
    should format a numeric value as a plain number, if isCurrency=false and decimals=null
    should format a numeric value as a number with n decimal places, if isCurrency=false and decimals=n
    should format a numeric values as a $#.## if isCurrency=true
*/
describe('on blur, the NumericInput component', () => {

    // it('should format a numeric value as a plain number, if isCurrency=false and decimals=null', () => {});

    // it('should format a numeric value as a number with n decimal places, if isCurrency=false and decimals=n', () => {});

    // it('should format a numeric values as a $#.## if isCurrency=true', () => {});

});

/* *****************************************************************************
the onChange handler for the NumericInput component
    should not be called on initialization
    should be called on edit when the value has changed
    should not be called on edit when the value has not changed
    should be called on blur when the value has changed
    should not be called on blur when the value has not changed
*/
describe('the onChange handler for the NumericInput component', () => {

    it('should not be called on initialization', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;

        mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);
    });

    it('should be called on edit when the value has changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = 99.9999;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(1);
        expect(onChange.calledWith(finalValue)).to.equal(true);
    });

    it('should not be called on edit when the value has not changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = initialValue;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(0);
    });

    it('should be called on blur when the value has changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = 4213.21;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(1);
        expect(onChange.calledWith(finalValue)).to.equal(true);
    });

    it('should not be called on blur when the value has not changed', () => {
        const onChange = sinon.spy();

        const required = true;
        const initialValue = 123.456;
        const finalValue = initialValue;

        const component = mount(
            <NumericInput
                required={required}
                value={initialValue}
                onChange={onChange}
            />
        );

        expect(onChange.callCount).to.equal(0);

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

        expect(onChange.callCount).to.equal(0);
    });
});

/* *****************************************************************************
the onValidation handler for the NumericInput component
    should be called on initialization with required=true, value=valid
    should be called on initialization with required=true, value=invalid
    should be called on initialization with required=true, value=blank
    should be called on initialization with required=false, value=valid
    should be called on initialization with required=false, value=invalid
    should be called on initialization with required=false, value=blank
    should not be called after change without prior blur event
    should be called on blur event
    should be called after change following a previous blur event
*/
describe('the onValidation handler for the NumericInput component', () => {

    const description = 'a numeric input';
    const expectedMessage = `${description} is required`;

    it('should be called on initialization with required=true, value=valid', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = 123.456;

        mount(
            <NumericInput
                required={required}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=true, value=invalid', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = 'this is not a number';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(false, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(expectedMessage, 'args[0][2]');
    });

    it('should be called on initialization with required=true, value=blank', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(false, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(expectedMessage, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=valid', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '101.9';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=invalid', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = 'woohoo!';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should be called on initialization with required=false, value=blank', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '';

        mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);
        expect(onValidation.args[0][0]).to.equal(false, 'args[0][0]');
        expect(onValidation.args[0][1]).to.equal(true, 'args[0][1]');
        expect(onValidation.args[0][2]).to.equal(null, 'args[0][2]');
    });

    it('should not be called after change without prior blur event', () => {
        const onValidation = sinon.spy();

        const required = false;
        const value = '';
        const newValue = '33.3';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(1);
    });

    it('should be called on blur event', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';
        const newValue = '33.3';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(2);
        expect(onValidation.args[1][0]).to.equal(true, 'args[1][0]');
        expect(onValidation.args[1][1]).to.equal(true, 'args[1][1]');
        expect(onValidation.args[1][2]).to.equal(null, 'args[1][2]');
    });

    it('should be called after change following a previous blur event', () => {
        const onValidation = sinon.spy();

        const required = true;
        const value = '';
        const newValue = '33.3';
        const finalValue = '';

        const component = mount(
            <NumericInput
                required={required}
                description={description}
                value={value}
                onValidation={onValidation}
            />
        );

        expect(onValidation.callCount).to.equal(1);

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

        expect(onValidation.callCount).to.equal(2);

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

        expect(onValidation.callCount).to.equal(3);

        expect(onValidation.args[2][0]).to.equal(true, 'args[2][0]');
        expect(onValidation.args[2][1]).to.equal(false, 'args[2][1]');
        expect(onValidation.args[2][2]).to.equal(expectedMessage, 'args[2][2]');
    });

});

/* *****************************************************************************
in terms of basic markup, the NumericInput component
    should be a div.form-group
    should include an input[type="text"]
    should include an input.form-control
    should include a label with the appropriate text if a label is specified
    should include a placeholder if a placeholder is specified
    should include a label with the the required flag if a label is specified & required is set
    should not include a label with the the required flag if a label is
        specified & required is not set
    should not include a label with the the required flag if a label is not
        specified & required is set
*/
describe('in terms of basic markup, the NumericInput component', () => {

    it('should be a div.form-group', () => {
        const component = shallow(<NumericInput />);

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

    it('should include an input[type="text"]', () => {
        const component = shallow(<NumericInput />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input[type="text"]').length).to.equal(1);
    });

    it('should include an input.form-control', () => {
        const component = shallow(<NumericInput />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input.form-control').length).to.equal(1);
    });

    it('should include a label with the appropriate text if a label is specified', () => {
        const label = 'some label';

        const component = shallow(<NumericInput label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().label).to.equal(label);
    });

    it('should include a placeholder if a placeholder is specified', () => {
        const placeholder = 'a number goes here';

        const component = shallow(<NumericInput placeholder={placeholder} />);

        expect(component.find('input').length).to.equal(1);
        expect(component.find('input').props().placeholder).to.equal(placeholder);
    });

    it('should include a label with the the required flag if a label is ' +
        'specified & required is set', () => {
        const label = 'some label';
        const required = true;

        const component = shallow(<NumericInput required={required} label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().required).to.equal(required);
    });

    it('should not include a label with the the required flag if a label is ' +
        'specified & required is not set', () => {
        const label = 'some label';
        const required = false;

        const component = shallow(<NumericInput required={required} label={label} />);

        expect(component.find('Label').length).to.equal(1);
        expect(component.find('Label').props().required).to.equal(required);
    });

    it('should not include a label with the the required flag if a label is ' +
        'not specified & required is set', () => {
        const required = false;

        const component = shallow(<NumericInput required={required} />);

        expect(component.find('Label').length).to.equal(0);
    });
});
<|endoftext|>


<commit_before>import React from 'react';
import './Record.scss';
import Attributes from '../Attribute/Attributes';
import DeleteView from '../DeleteView/DeleteView';
import Dropdown from 'components/Dropdown';
import Popup from 'components/Popup';

export class Record extends React.Component {
  constructor (props) {
    super(props);

    this.renderRecordButtons = this.renderRecordButtons.bind(this);
    this.toggleAttributeVisibility = this.toggleAttributeVisibility.bind(this);
    this.updateTypeSpecifier = this.updateTypeSpecifier.bind(this);
    this.updateRecordType = this.updateRecordType.bind(this);
    this.updateRecordAttribute = this.updateRecordAttribute.bind(this);
    this.state = {
      showAttributes: false,
      mode: 'view',
      deleting: false,
      typeSpecifier: this.props.record.attributes.TypeSpecifier,
      type: this.props.record.attributes.RecordType,
      attributes: this.props.record.attributes
    };
  }

  setMode (value) {
    this.setState({ mode: value });
  }

  toggleAttributeVisibility () {
    const currentVisibility = this.state.showAttributes;
    const newVisibility = !currentVisibility;
    this.setState({ showAttributes: newVisibility });
  }

  updateTypeSpecifier (typeSpecifier, recordId) {
    this.setState({
      typeSpecifier: typeSpecifier
    });
    const updatedTypeSpecifier = {
      typeSpecifier: typeSpecifier,
      recordId: recordId
    };
    this.props.editRecordAttribute(updatedTypeSpecifier);
  }

  updateRecordType (attribute, recordId) {
    const updatedRecordType = {
      type: attribute,
      recordId: recordId
    };
    this.props.editRecordAttribute(updatedRecordType);
  }

  updateRecordAttribute (attribute, attributeIndex, recordId) {
    this.setState({
      attributes: {
        [attributeIndex]: attribute
      }
    });
    const updatedRecordAttribute = { attribute, attributeIndex, recordId };
    this.props.editRecordAttribute(updatedRecordAttribute);
  }

  cancelDeletion () {
    this.setState({ deleting: false });
  }

  delete () {
    this.setState({ deleting: false });
    this.props.removeRecord(this.props.record.id, this.props.record.action);
  }

  renderRecordButtons () {
    if (this.state.mode === 'view') {
      return (

          { this.props.documentState === 'edit' &&
          <Dropdown
            children={[
              {
                text: 'Muokkaa asiakirjaa',
                icon: 'fa-pencil',
                style: 'btn-primary',
                action: () => this.props.editRecordForm(
                  this.props.record.id,
                  this.state.typeSpecifier,
                  this.state.attributes
                )
              },
              {
                text: 'Täydennä metatietoja',
                icon: 'fa-plus-square',
                style: 'btn-primary',
                action: () => this.props.complementRecordForm(
                  this.props.record.id,
                  this.state.typeSpecifier,
                  this.state.attributes
                )
              },
              {
                text: 'Poista asiakirja',
                icon: 'fa-trash',
                style: 'btn-delete',
                action: () => this.setState({ deleting: true })
              }
            ]}
            extraSmall={true}
          />
          }
          <button
            className='btn btn-info btn-xs record-button pull-right'
            onClick={this.toggleAttributeVisibility}>

          </button>

      );
    }
  }

  render () {
    const { attributeTypes, recordTypes, record, documentState } = this.props;
    return (

        <Attributes
          element={record}
          documentState={documentState}
          mode={this.state.mode}
          type={'record'}
          attributeTypes={attributeTypes}
          typeOptions={recordTypes}
          renderButtons={this.renderRecordButtons}
          updateTypeSpecifier={this.updateTypeSpecifier}
          updateType={this.updateRecordType}
          updateAttribute={this.updateRecordAttribute}
          showAttributes={this.state.showAttributes}
        />
        { this.state.deleting &&
        <Popup
          content={
            <DeleteView
              type='record'
              target={record.attributes.TypeSpecifier}
              action={() => this.delete()}
              cancel={() => this.cancelDeletion()}
            />
          }
          closePopup={() => this.cancelDeletion()}
        />
        }

    );
  }
}

Record.propTypes = {
  attributeTypes: React.PropTypes.object.isRequired,
  complementRecordForm: React.PropTypes.func.isRequired,
  documentState: React.PropTypes.string.isRequired,
  editRecordAttribute: React.PropTypes.func.isRequired,
  editRecordForm: React.PropTypes.func.isRequired,
  record: React.PropTypes.object.isRequired,
  recordTypes: React.PropTypes.object.isRequired,
  removeRecord: React.PropTypes.func.isRequired
};

export default Record;
<commit_msg>Rename variable<commit_after>import React from 'react';
import './Record.scss';
import Attributes from '../Attribute/Attributes';
import DeleteView from '../DeleteView/DeleteView';
import Dropdown from 'components/Dropdown';
import Popup from 'components/Popup';

export class Record extends React.Component {
  constructor (props) {
    super(props);

    this.renderRecordButtons = this.renderRecordButtons.bind(this);
    this.toggleAttributeVisibility = this.toggleAttributeVisibility.bind(this);
    this.updateTypeSpecifier = this.updateTypeSpecifier.bind(this);
    this.updateRecordType = this.updateRecordType.bind(this);
    this.updateRecordAttribute = this.updateRecordAttribute.bind(this);
    this.state = {
      showAttributes: false,
      mode: 'view',
      deleting: false,
      typeSpecifier: this.props.record.attributes.TypeSpecifier,
      type: this.props.record.attributes.RecordType,
      attributes: this.props.record.attributes
    };
  }

  setMode (value) {
    this.setState({ mode: value });
  }

  toggleAttributeVisibility () {
    const currentVisibility = this.state.showAttributes;
    const newVisibility = !currentVisibility;
    this.setState({ showAttributes: newVisibility });
  }

  updateTypeSpecifier (typeSpecifier, recordId) {
    this.setState({
      typeSpecifier: typeSpecifier
    });
    const updatedTypeSpecifier = {
      typeSpecifier: typeSpecifier,
      recordId: recordId
    };
    this.props.editRecordAttribute(updatedTypeSpecifier);
  }

  updateRecordType (type, recordId) {
    this.setState({
      type: type
    });
    const updatedRecordType = {
      type: type,
      recordId: recordId
    };
    this.props.editRecordAttribute(updatedRecordType);
  }

  updateRecordAttribute (attribute, attributeIndex, recordId) {
    this.setState({
      attributes: {
        [attributeIndex]: attribute
      }
    });
    const updatedRecordAttribute = { attribute, attributeIndex, recordId };
    this.props.editRecordAttribute(updatedRecordAttribute);
  }

  cancelDeletion () {
    this.setState({ deleting: false });
  }

  delete () {
    this.setState({ deleting: false });
    this.props.removeRecord(this.props.record.id, this.props.record.action);
  }

  renderRecordButtons () {
    if (this.state.mode === 'view') {
      return (

          { this.props.documentState === 'edit' &&
          <Dropdown
            children={[
              {
                text: 'Muokkaa asiakirjaa',
                icon: 'fa-pencil',
                style: 'btn-primary',
                action: () => this.props.editRecordForm(
                  this.props.record.id,
                  this.state.typeSpecifier,
                  this.state.attributes
                )
              },
              {
                text: 'Täydennä metatietoja',
                icon: 'fa-plus-square',
                style: 'btn-primary',
                action: () => this.props.complementRecordForm(
                  this.props.record.id,
                  this.state.typeSpecifier,
                  this.state.attributes
                )
              },
              {
                text: 'Poista asiakirja',
                icon: 'fa-trash',
                style: 'btn-delete',
                action: () => this.setState({ deleting: true })
              }
            ]}
            extraSmall={true}
          />
          }
          <button
            className='btn btn-info btn-xs record-button pull-right'
            onClick={this.toggleAttributeVisibility}>

          </button>

      );
    }
  }

  render () {
    const { attributeTypes, recordTypes, record, documentState } = this.props;
    return (

        <Attributes
          element={record}
          documentState={documentState}
          mode={this.state.mode}
          type={'record'}
          attributeTypes={attributeTypes}
          typeOptions={recordTypes}
          renderButtons={this.renderRecordButtons}
          updateTypeSpecifier={this.updateTypeSpecifier}
          updateType={this.updateRecordType}
          updateAttribute={this.updateRecordAttribute}
          showAttributes={this.state.showAttributes}
        />
        { this.state.deleting &&
        <Popup
          content={
            <DeleteView
              type='record'
              target={record.attributes.TypeSpecifier}
              action={() => this.delete()}
              cancel={() => this.cancelDeletion()}
            />
          }
          closePopup={() => this.cancelDeletion()}
        />
        }

    );
  }
}

Record.propTypes = {
  attributeTypes: React.PropTypes.object.isRequired,
  complementRecordForm: React.PropTypes.func.isRequired,
  documentState: React.PropTypes.string.isRequired,
  editRecordAttribute: React.PropTypes.func.isRequired,
  editRecordForm: React.PropTypes.func.isRequired,
  record: React.PropTypes.object.isRequired,
  recordTypes: React.PropTypes.object.isRequired,
  removeRecord: React.PropTypes.func.isRequired
};

export default Record;
<|endoftext|>


<commit_before>/*
 This file is a part of libertysoil.org website
 Copyright (C) 2015  Loki Education (Social Enterprise)

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import React, { PropTypes, Component } from 'react';

import ModalComponent from '../components/modal-component';
import TagCloud from '../components/tag-cloud';
import TagIcon from '../components/tag-icon';
import * as TagType from '../utils/tags';

export default class AddHashtagModal extends Component {
  static displayName = 'AddTagModal';

  static propTypes = {
    locations: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    schools: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    tags: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    onClose: PropTypes.func,
    onSave: PropTypes.func
  };

  static defaultProps = {
    locations: [],
    schools: [],
    tags: [],
    onClose: () => {},
    onSave: () => {}
  };

  constructor(props) {
    super(props);

    this.state = {
      activeTabIndex: 0,
      tags: props.tags
    };
  }

  selectTab = (index) => {
    this.setState({ activeTabIndex: index });
  };

  _handleAddTag = () => {
    let tags = this.state.tags;
    let tagName = this.refs.input.value.trim();

    if (tagName.length == 0) {
      return;
    }

    if (tags.find(tag => tag.name === tagName)) {
      return;
    }

    this.refs.input.value = '';

    tags.push({name: tagName});
    this.setState({tags});
  };

  _save = () => {
    this.props.onSave({tags: this.state.tags});
  };

  render () {
    let {
      locations,
      schools,
      tags,
      onClose
    } = this.props;

    let shouldRenderAddedTags = schools.length > 0 || locations.length > 0 || tags.length > 0;

    return (
      <ModalComponent
        className="add_tag_modal add_tag_modal-hashtag"
        size="normal"
        onHide={onClose}
      >
        <ModalComponent.Head>
          <ModalComponent.Title>Add hashtags to your post</ModalComponent.Title>
          <TagIcon big type={TagType.TAG_HASHTAG} />
        </ModalComponent.Head>
        <ModalComponent.Body>

Enter manually

                  <input ref="input" type="text" placeholder="Start typing..." className="input input-block input-transparent input-button_height" />

                  Add

            {shouldRenderAddedTags &&

                  Added:

                  <TagCloud
                    tags={this.state.tags}
                  />

            }

        </ModalComponent.Body>
        <ModalComponent.Actions>

Save

        </ModalComponent.Actions>
      </ModalComponent>
    );
  }
}
<commit_msg>Refactor AddHashtagModal<commit_after>/*
 This file is a part of libertysoil.org website
 Copyright (C) 2015  Loki Education (Social Enterprise)

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import React, { PropTypes, Component } from 'react';
import _ from 'lodash';

import ModalComponent from '../components/modal-component';
import TagEditor from './add-tag-modal/tag-editor';
import TagIcon from '../components/tag-icon';
import * as TagType from '../utils/tags';

export default class AddHashtagModal extends Component {
  static displayName = 'AddHashtagModal';

  static propTypes = {
    locations: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    onClose: PropTypes.func,
    onSave: PropTypes.func,
    schools: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    tags: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string
    })),
    visible: PropTypes.bool
  };

  static defaultProps = {
    locations: [],
    schools: [],
    tags: [],
    visible: false,
    onClose: () => {},
    onSave: () => {}
  };

  _handleAddTag = () => {
    let tags = this._tagEditor.getTags().tags;
    let tagName = this.refs.input.value.trim();

    if (tagName.length < 3) {
      return;
    }

    if (tags.find(tag => tag.name === tagName)) {
      return;
    }

    this.refs.input.value = '';

    this._tagEditor.addTag({name: tagName});
  };

  _save = () => {
    this.props.onSave(_.pick(this.state, 'locations', 'schools', 'tags'));
  };

  render () {
    let {
      locations,
      schools,
      tags,
      visible,
      onClose
    } = this.props;

    if (!visible) {
      return null;
    }

    return (
      <ModalComponent
        className="add_tag_modal add_tag_modal-hashtag"
        size="normal"
        onHide={onClose}
      >
        <ModalComponent.Head>
          <ModalComponent.Title>Add hashtags to your post</ModalComponent.Title>
          <TagIcon big type={TagType.TAG_HASHTAG} />
        </ModalComponent.Head>
        <ModalComponent.Body>

Enter manually

                  <input
                    className="input input-block input-transparent input-button_height"
                    placeholder="Start typing..."
                    ref="input"
                    type="text"
                  />

                    Add

            <TagEditor
              locations={locations}
              ref={(c) => this._tagEditor = c}
              schools={schools}
              tags={tags}
            />

        </ModalComponent.Body>
        <ModalComponent.Actions>

Save

        </ModalComponent.Actions>
      </ModalComponent>
    );
  }
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { toastr } from 'react-redux-toastr';
import { Link, Router } from 'routes';

// Redux
import { connect } from 'react-redux';
import { toggleModal, setModalOptions } from 'redactions/modal';
import { toggleTooltip } from 'redactions/tooltip';

// Components
import Spinner from 'components/ui/Spinner';
import Map from 'components/widgets/editor/map/Map';
import AreaSubscriptionModal from 'components/modal/AreaSubscriptionModal';
import AreaActionsTooltip from 'components/areas/AreaActionsTooltip';

// Services
import DatasetService from 'services/DatasetService';
import AreasService from 'services/AreasService';
import UserService from 'services/UserService';

// Utils
import LayerManager from 'components/widgets/editor/helpers/LayerManager';

const MAP_CONFIG = {
  zoom: 3,
  latLng: {
    lat: 0,
    lng: 0
  },
  zoomControl: false
};

class AreaCard extends React.Component {
  /**
   * Return the position of the click within the page taking
   * into account the scroll (relative to the page, not the
   * viewport )
   * @static
   * @param {MouseEvent} e Event
   * @returns {{ x: number, y: number }}
   */
  static getClickPosition(e) {
    return {
      x: window.scrollX + e.clientX,
      y: window.scrollY + e.clientY
    };
  }

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      country: null,
      layer: {}
    };

    // Services
    this.datasetService = new DatasetService(null, {
      apiURL: process.env.WRI_API_URL,
      language: props.locale
    });
    this.areasService = new AreasService({ apiURL: process.env.WRI_API_URL });
    this.userService = new UserService({ apiURL: process.env.WRI_API_URL });

    // ------------------- Bindings -----------------------
    this.handleEditSubscription = this.handleEditSubscription.bind(this);
    this.handleSubscriptionCreated = this.handleSubscriptionCreated.bind(this);
    this.handleSubscriptionUpdated = this.handleSubscriptionUpdated.bind(this);
    this.handleDeleteArea = this.handleDeleteArea.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
    // ----------------------------------------------------
  }

  componentDidMount() {
    const {
      openSubscriptionsModal,
      subscriptionThreshold,
      subscriptionType,
      subscriptionDataset
    } = this.props;
    this.loadData();
    if (openSubscriptionsModal) {
      this.handleEditSubscription(subscriptionDataset, subscriptionType, subscriptionThreshold);
    }
  }

  loadData() {
    this.setState({ loading: true });
    const attsObj = this.props.area.attributes;

    if (attsObj.geostore) {
      this.areasService.getGeostore(attsObj.geostore)
        .then((res) => {
          const obj = res.data;
          const bounds = [
            [obj.attributes.bbox[0], obj.attributes.bbox[1]],
            [obj.attributes.bbox[2], obj.attributes.bbox[3]]
          ];
          const fakeLayer = {
            id: `${obj.id}`,
            provider: 'geojson',
            active: true,
            layerConfig: {
              data: obj.attributes.geojson,
              fitBounds: true,
              bounds: { type: 'Polygon', coordinates: [bounds] }
            }
          };

          this.setState({
            loading: false,
            country: obj.id,
            layerGroups: [{
              dataset: null,
              visible: true,
              layers: [fakeLayer]
            }]
          });
        });
    } else if (attsObj.iso.country) {
      this.areasService.getCountry(attsObj.iso.country)
        .then((res) => {
          const country = res.data[0];
          const newGeoJson = {
            type: 'FeatureCollection',
            features: [
              {
                type: 'Feature',
                properties: {},
                geometry: JSON.parse(country.geojson)
              }
            ]
          };

          const fakeLayer = {
            id: `-${country.label}`,
            provider: 'geojson',
            active: true,
            layerConfig: {
              data: newGeoJson,
              fitBounds: true,
              bounds: JSON.parse(country.bounds)
            }
          };

          this.setState({
            loading: false,
            country: country.label,
            layerGroups: [{
              dataset: null,
              visible: true,
              layers: [fakeLayer]
            }]
          });
        });
    }
  }

  handleEditArea() {
    Router.pushRoute('myrw_detail', { id: this.props.area.id, tab: 'areas' });
  }

  handleEditSubscription(
    subscriptionDataset = null,
    subscriptionType = null,
    subscriptionThreshold = null
  ) {
    const mode = this.props.area.subscription ? 'edit' : 'new';
    const options = {
      children: AreaSubscriptionModal,
      childrenProps: {
        area: this.props.area,
        toggleModal: this.props.toggleModal,
        onSubscriptionUpdated: this.handleSubscriptionUpdated,
        onSubscriptionCreated: this.handleSubscriptionUpdated,
        mode,
        subscriptionDataset,
        subscriptionType,
        subscriptionThreshold
      }
    };
    this.props.toggleModal(true);
    this.props.setModalOptions(options);
  }

  handleSubscriptionCreated() {
    this.props.onChange();
  }

  handleSubscriptionUpdated() {
    this.props.onChange();
  }

  handleDeleteArea() {
    const { area, token } = this.props;
    const toastrConfirmOptions = {
      onOk: () => {
        this.setState({ loading: true });

        // Delete subscription associated to area if there's one
        if (area.subscription) {
          this.userService.deleteSubscription(area.subscription.id, token)
            .then(() => {
              this.userService.deleteArea(area.id, token)
                .then(() => {
                  this.props.onAreaRemoved();
                })
                // Fetch throws an error for some reason but the request is successful...
                .catch(err => this.props.onAreaRemoved()); // eslint-disable-line
            })
            .catch((err) => {
              this.setState({ loading: false });
              toastr.error('Error removing area', err);
            });
        } else {
          this.userService.deleteArea(area.id, token)
            .then(() => {
              this.props.onAreaRemoved();
            })
            // Fetch throws an error for some reason but the request is successful...
            .catch(err => this.props.onAreaRemoved()); // eslint-disable-line
        }
      }
    };
    toastr.confirm(`Are you sure you want to delete the area ${area.attributes.name}?
      Deleting an area will delete all the subscriptions associated to it`, toastrConfirmOptions);
  }

  handleEdit(event) {
    const position = AreaCard.getClickPosition(event);
    this.props.toggleTooltip(true, {
      follow: false,
      position,
      children: AreaActionsTooltip,
      childrenProps: {
        toggleTooltip: this.props.toggleTooltip,
        onEditArea: this.handleEditArea,
        onEditSubscriptions: this.handleEditSubscription
      }
    });
  }

  render() {
    const { loading, layerGroups } = this.state;
    const { area } = this.props;
    const name = area.attributes.name;
    const subscription = area.subscription;
    const subscriptionConfirmed = area.subscription && area.subscription.attributes.confirmed;

    const borderContainerClassNames = classnames({
      'border-container': true,
      'blue-background': subscription && !subscriptionConfirmed
    });

    return (

            <Map
              LayerManager={LayerManager}
              mapConfig={MAP_CONFIG}
              layerGroups={layerGroups || []}
              interactionEnabled={false}
              useLightBasemap
            />

          <Spinner isLoading={loading} className="-small -light -relative -center" />

{name}

              {subscription &&

                    {subscription.attributes.datasets.map((datasetObj, index) =>
                      (<div
                        className="dataset-element"
                        key={`${datasetObj}-${index}`} // eslint-disable-line
                      >

                          <Link
                            route={'explore_detail'}
                            params={{ id: datasetObj.id }}
                          >

                              {datasetObj.label}

                          </Link>

                          {subscription.attributes.datasetsQuery
                            .find(elem => elem.id === datasetObj.id).type}
                           (≥{subscription.attributes.datasetsQuery
                            .find(elem => elem.id === datasetObj.id).threshold})

)
                    )}

              }
              {subscription &&

                    {!subscriptionConfirmed &&

                      Pending

                    }

              }

              <button
                className="c-btn -b -compressed"
                onClick={this.handleEdit}
              >
                Edit
              </button>

                Delete

    );
  }
}

AreaCard.defaultProps = {
  openSubscriptionsModal: false
};

AreaCard.propTypes = {
  token: PropTypes.string.isRequired,
  area: PropTypes.object.isRequired,
  locale: PropTypes.string.isRequired,
  openSubscriptionsModal: PropTypes.bool,
  // Callbacks
  onAreaRemoved: PropTypes.func.isRequired,
  onChange: PropTypes.func.isRequired,
  // Store
  toggleModal: PropTypes.func.isRequired,
  setModalOptions: PropTypes.func.isRequired,
  toggleTooltip: PropTypes.func.isRequired
};

const mapStateToProps = state => ({
  locale: state.common.locale
});

const mapDispatchToProps = dispatch => ({
  toggleModal: (open, opts) => { dispatch(toggleModal(open, opts)); },
  setModalOptions: (options) => { dispatch(setModalOptions(options)); },
  toggleTooltip: (opened, opts) => {
    dispatch(toggleTooltip(opened, opts));
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(AreaCard);
<commit_msg>Add missing binding plus missing proptypes<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { toastr } from 'react-redux-toastr';
import { Link, Router } from 'routes';

// Redux
import { connect } from 'react-redux';
import { toggleModal, setModalOptions } from 'redactions/modal';
import { toggleTooltip } from 'redactions/tooltip';

// Components
import Spinner from 'components/ui/Spinner';
import Map from 'components/widgets/editor/map/Map';
import AreaSubscriptionModal from 'components/modal/AreaSubscriptionModal';
import AreaActionsTooltip from 'components/areas/AreaActionsTooltip';

// Services
import DatasetService from 'services/DatasetService';
import AreasService from 'services/AreasService';
import UserService from 'services/UserService';

// Utils
import LayerManager from 'components/widgets/editor/helpers/LayerManager';

const MAP_CONFIG = {
  zoom: 3,
  latLng: {
    lat: 0,
    lng: 0
  },
  zoomControl: false
};

class AreaCard extends React.Component {
  /**
   * Return the position of the click within the page taking
   * into account the scroll (relative to the page, not the
   * viewport )
   * @static
   * @param {MouseEvent} e Event
   * @returns {{ x: number, y: number }}
   */
  static getClickPosition(e) {
    return {
      x: window.scrollX + e.clientX,
      y: window.scrollY + e.clientY
    };
  }

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      country: null,
      layer: {}
    };

    // Services
    this.datasetService = new DatasetService(null, {
      apiURL: process.env.WRI_API_URL,
      language: props.locale
    });
    this.areasService = new AreasService({ apiURL: process.env.WRI_API_URL });
    this.userService = new UserService({ apiURL: process.env.WRI_API_URL });

    // ------------------- Bindings -----------------------
    this.handleEditSubscription = this.handleEditSubscription.bind(this);
    this.handleSubscriptionCreated = this.handleSubscriptionCreated.bind(this);
    this.handleSubscriptionUpdated = this.handleSubscriptionUpdated.bind(this);
    this.handleDeleteArea = this.handleDeleteArea.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
    this.handleEditArea = this.handleEditArea.bind(this);
    // ----------------------------------------------------
  }

  componentDidMount() {
    const {
      openSubscriptionsModal,
      subscriptionThreshold,
      subscriptionType,
      subscriptionDataset
    } = this.props;
    this.loadData();
    if (openSubscriptionsModal) {
      this.handleEditSubscription(subscriptionDataset, subscriptionType, subscriptionThreshold);
    }
  }

  loadData() {
    this.setState({ loading: true });
    const attsObj = this.props.area.attributes;

    if (attsObj.geostore) {
      this.areasService.getGeostore(attsObj.geostore)
        .then((res) => {
          const obj = res.data;
          const bounds = [
            [obj.attributes.bbox[0], obj.attributes.bbox[1]],
            [obj.attributes.bbox[2], obj.attributes.bbox[3]]
          ];
          const fakeLayer = {
            id: `${obj.id}`,
            provider: 'geojson',
            active: true,
            layerConfig: {
              data: obj.attributes.geojson,
              fitBounds: true,
              bounds: { type: 'Polygon', coordinates: [bounds] }
            }
          };

          this.setState({
            loading: false,
            country: obj.id,
            layerGroups: [{
              dataset: null,
              visible: true,
              layers: [fakeLayer]
            }]
          });
        });
    } else if (attsObj.iso.country) {
      this.areasService.getCountry(attsObj.iso.country)
        .then((res) => {
          const country = res.data[0];
          const newGeoJson = {
            type: 'FeatureCollection',
            features: [
              {
                type: 'Feature',
                properties: {},
                geometry: JSON.parse(country.geojson)
              }
            ]
          };

          const fakeLayer = {
            id: `-${country.label}`,
            provider: 'geojson',
            active: true,
            layerConfig: {
              data: newGeoJson,
              fitBounds: true,
              bounds: JSON.parse(country.bounds)
            }
          };

          this.setState({
            loading: false,
            country: country.label,
            layerGroups: [{
              dataset: null,
              visible: true,
              layers: [fakeLayer]
            }]
          });
        });
    }
  }

  handleEditArea() {
    Router.pushRoute('myrw_detail', { id: this.props.area.id, tab: 'areas' });
  }

  handleEditSubscription(
    subscriptionDataset = null,
    subscriptionType = null,
    subscriptionThreshold = null
  ) {
    const mode = this.props.area.subscription ? 'edit' : 'new';
    const options = {
      children: AreaSubscriptionModal,
      childrenProps: {
        area: this.props.area,
        toggleModal: this.props.toggleModal,
        onSubscriptionUpdated: this.handleSubscriptionUpdated,
        onSubscriptionCreated: this.handleSubscriptionUpdated,
        mode,
        subscriptionDataset,
        subscriptionType,
        subscriptionThreshold
      }
    };
    this.props.toggleModal(true);
    this.props.setModalOptions(options);
  }

  handleSubscriptionCreated() {
    this.props.onChange();
  }

  handleSubscriptionUpdated() {
    this.props.onChange();
  }

  handleDeleteArea() {
    const { area, token } = this.props;
    const toastrConfirmOptions = {
      onOk: () => {
        this.setState({ loading: true });

        // Delete subscription associated to area if there's one
        if (area.subscription) {
          this.userService.deleteSubscription(area.subscription.id, token)
            .then(() => {
              this.userService.deleteArea(area.id, token)
                .then(() => {
                  this.props.onAreaRemoved();
                })
                // Fetch throws an error for some reason but the request is successful...
                .catch(err => this.props.onAreaRemoved()); // eslint-disable-line
            })
            .catch((err) => {
              this.setState({ loading: false });
              toastr.error('Error removing area', err);
            });
        } else {
          this.userService.deleteArea(area.id, token)
            .then(() => {
              this.props.onAreaRemoved();
            })
            // Fetch throws an error for some reason but the request is successful...
            .catch(err => this.props.onAreaRemoved()); // eslint-disable-line
        }
      }
    };
    toastr.confirm(`Are you sure you want to delete the area ${area.attributes.name}?
      Deleting an area will delete all the subscriptions associated to it`, toastrConfirmOptions);
  }

  handleEdit(event) {
    const position = AreaCard.getClickPosition(event);
    this.props.toggleTooltip(true, {
      follow: false,
      position,
      children: AreaActionsTooltip,
      childrenProps: {
        toggleTooltip: this.props.toggleTooltip,
        onEditArea: this.handleEditArea,
        onEditSubscriptions: this.handleEditSubscription
      }
    });
  }

  render() {
    const { loading, layerGroups } = this.state;
    const { area } = this.props;
    const name = area.attributes.name;
    const subscription = area.subscription;
    const subscriptionConfirmed = area.subscription && area.subscription.attributes.confirmed;

    const borderContainerClassNames = classnames({
      'border-container': true,
      'blue-background': subscription && !subscriptionConfirmed
    });

    return (

            <Map
              LayerManager={LayerManager}
              mapConfig={MAP_CONFIG}
              layerGroups={layerGroups || []}
              interactionEnabled={false}
              useLightBasemap
            />

          <Spinner isLoading={loading} className="-small -light -relative -center" />

{name}

              {subscription &&

                    {subscription.attributes.datasets.map((datasetObj, index) =>
                      (<div
                        className="dataset-element"
                        key={`${datasetObj}-${index}`} // eslint-disable-line
                      >

                          <Link
                            route={'explore_detail'}
                            params={{ id: datasetObj.id }}
                          >

                              {datasetObj.label}

                          </Link>

                          {subscription.attributes.datasetsQuery
                            .find(elem => elem.id === datasetObj.id).type}
                           (≥{subscription.attributes.datasetsQuery
                            .find(elem => elem.id === datasetObj.id).threshold})

)
                    )}

              }
              {subscription &&

                    {!subscriptionConfirmed &&

                      Pending

                    }

              }

              <button
                className="c-btn -b -compressed"
                onClick={this.handleEdit}
              >
                Edit
              </button>

                Delete

    );
  }
}

AreaCard.defaultProps = {
  openSubscriptionsModal: false
};

AreaCard.propTypes = {
  token: PropTypes.string.isRequired,
  area: PropTypes.object.isRequired,
  locale: PropTypes.string.isRequired,
  openSubscriptionsModal: PropTypes.bool,
  subscriptionThreshold: PropTypes.number,
  subscriptionType: PropTypes.string,
  subscriptionDataset: PropTypes.string,
  // Callbacks
  onAreaRemoved: PropTypes.func.isRequired,
  onChange: PropTypes.func.isRequired,
  // Store
  toggleModal: PropTypes.func.isRequired,
  setModalOptions: PropTypes.func.isRequired,
  toggleTooltip: PropTypes.func.isRequired
};

const mapStateToProps = state => ({
  locale: state.common.locale
});

const mapDispatchToProps = dispatch => ({
  toggleModal: (open, opts) => { dispatch(toggleModal(open, opts)); },
  setModalOptions: (options) => { dispatch(setModalOptions(options)); },
  toggleTooltip: (opened, opts) => {
    dispatch(toggleTooltip(opened, opts));
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(AreaCard);
<|endoftext|>


<commit_before>import observeDom from '../../utils/observe-dom'
import KeyCodes from '../../utils/key-codes'
import { selectAll, reflow, addClass, removeClass, setAttr, eventOn, eventOff } from '../../utils/dom'
import idMixin from '../../mixins/id'

// Slide directional classes
const DIRECTION = {
  next: {
    dirClass: 'carousel-item-left',
    overlayClass: 'carousel-item-next'
  },
  prev: {
    dirClass: 'carousel-item-right',
    overlayClass: 'carousel-item-prev'
  }
}

// Fallback Transition duration (with a little buffer) in ms
const TRANS_DURATION = 600 + 50

// Transition Event names
const TransitionEndEvents = {
  WebkitTransition: 'webkitTransitionEnd',
  MozTransition: 'transitionend',
  OTransition: 'otransitionend oTransitionEnd',
  transition: 'transitionend'
}

// Return the browser specific transitionEnd event name
function getTransisionEndEvent (el) {
  for (const name in TransitionEndEvents) {
    if (el.style[name] !== undefined) {
      return TransitionEndEvents[name]
    }
  }
  // fallback
  return null
}

export default {
  mixins: [ idMixin ],
  render (h) {
    // Wrapper for slides
    const inner = h(
      'div',
      {
        ref: 'inner',
        class: [ 'carousel-inner' ],
        attrs: {
          id: this.safeId('__BV_inner_'),
          role: 'list'
        }
      },
      [ this.$slots.default ]
    )

    // Prev and Next Controls
    let controls = h(false)
    if (this.controls) {
      controls = [
        h(
          'a',
          {
            class: [ 'carousel-control-prev' ],
            attrs: { href: '#', role: 'button', 'aria-controls': this.safeId('__BV_inner_') },
            on: {
              click: (evt) => {
                evt.preventDefault()
                evt.stopPropagation()
                this.prev()
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.prev()
                }
              }
            }
          },
          [
            h('span', { class: [ 'carousel-control-prev-icon' ], attrs: { 'aria-hidden': 'true' } }),
            h('span', { class: [ 'sr-only' ] }, [ this.labelPrev ])
          ]
        ),
        h(
          'a',
          {
            class: [ 'carousel-control-next' ],
            attrs: { href: '#', role: 'button', 'aria-controls': this.safeId('__BV_inner_') },
            on: {
              click: (evt) => {
                evt.preventDefault()
                evt.stopPropagation()
                this.next()
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.next()
                }
              }
            }
          },
          [
            h('span', { class: [ 'carousel-control-next-icon' ], attrs: { 'aria-hidden': 'true' } }),
            h('span', { class: [ 'sr-only' ] }, [ this.labelNext ])
          ]
        )
      ]
    }

    // Indicators
    const indicators = h(
      'ol',
      {
        class: [ 'carousel-indicators' ],
        directives: [
          { name: 'show', rawName: 'v-show', value: this.indicators, expression: 'indicators' }
        ],
        attrs: {
          id: this.safeId('__BV_indicators_'),
          'aria-hidden': this.indicators ? 'false' : 'true',
          'aria-label': this.labelIndicators,
          'aria-owns': this.safeId('__BV_inner_')
        }
      },
      this.slides.map((slide, n) => {
        return h(
          'li',
          {
            key: `slide_${n}`,
            class: { active: n === this.index },
            attrs: {
              role: 'button',
              id: this.safeId(`__BV_indicator_${n + 1}_`),
              tabindex: this.indicators ? '0' : '-1',
              'aria-current': n === this.index ? 'true' : 'false',
              'aria-label': `${this.labelGotoSlide} ${n + 1}`,
              'aria-describedby': this.slides[n].id || null,
              'aria-controls': this.safeId('__BV_inner_')
            },
            on: {
              click: (evt) => {
                this.setSlide(n)
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.setSlide(n)
                }
              }
            }
          }
        )
      })
    )

    // Return the carousel
    return h(
      'div',
      {
        class: [ 'carousel', 'slide' ],
        style: { background: this.background },
        attrs: {
          role: 'region',
          id: this.safeId(),
          'aria-busy': this.isSliding ? 'true' : 'false'
        },
        on: {
          mouseenter: this.pause,
          mouseleave: this.restart,
          focusin: this.pause,
          focusout: this.restart,
          keydown: (evt) => {
            const keyCode = evt.keyCode
            if (keyCode === KeyCodes.LEFT || keyCode === KeyCodes.RIGHT) {
              evt.preventDefault()
              evt.stopPropagation()
              t[keyCode === KeyCodes.LEFT ? 'prev' : 'next']()
            }
          }
        }
      },
      [ inner, controls, indicators ]
    )
  },
  data () {
    return {
      index: this.value || 0,
      isSliding: false,
      intervalId: null,
      transitionEndEvent: null,
      slides: [],
      direction: null
    }
  },
  props: {
    labelPrev: {
      type: String,
      default: 'Previous Slide'
    },
    labelNext: {
      type: String,
      default: 'Next Slide'
    },
    labelGotoSlide: {
      type: String,
      default: 'Goto Slide'
    },
    labelIndicators: {
      type: String,
      default: 'Select a slide to display'
    },
    interval: {
      type: Number,
      default: 5000
    },
    indicators: {
      type: Boolean,
      default: false
    },
    controls: {
      type: Boolean,
      default: false
    },
    imgWidth: {
      // Sniffed by carousel-slide
      type: [Number, String]
    },
    imgHeight: {
      // Sniffed by carousel-slide
      type: [Number, String]
    },
    background: {
      type: String
    },
    value: {
      type: Number,
      default: 0
    }
  },
  computed: {
    isCycling () {
      return Boolean(this.intervalId)
    }
  },
  methods: {
    // Set slide
    setSlide (slide) {
      // Don't animate when page is not visible
      if (typeof document !== 'undefined' && document.visibilityState && document.hidden) {
        return
      }
      const len = this.slides.length
      // Don't do anything if nothing to slide to
      if (len === 0) {
        return
      }
      // Don't change slide while transitioning, wait until transition is done
      if (this.isSliding) {
        // Schedule slide after sliding complete
        this.$once('sliding-end', () => this.setSlide(slide))
        return
      }
      // Make sure we have an integer (you never know!)
      slide = Math.floor(slide)
      // Set new slide index. Wrap around if necessary
      this.index = slide >= len ? 0 : (slide >= 0 ? slide : len - 1)
    },
    // Previous slide
    prev () {
      this.direction = 'prev'
      this.setSlide(this.index - 1)
    },
    // Next slide
    next () {
      this.direction = 'next'
      this.setSlide(this.index + 1)
    },
    // Pause auto rotation
    pause () {
      if (this.isCycling) {
        clearInterval(this.intervalId)
        this.intervalId = null
        if (this.slides[this.index]) {
          // Make current slide focusable for screen readers
          this.slides[this.index].tabIndex = 0
        }
      }
    },
    // Start auto rotate slides
    start () {
      // Don't start if no interval, or if we are already running
      if (!this.interval || this.isCycling) {
        return
      }
      this.slides.forEach(slide => {
        slide.tabIndex = -1
      })
      this.intervalId = setInterval(() => {
        this.next()
      }, Math.max(1000, this.interval))
    },
    // Re-Start auto rotate slides when focus/hover leaves the carousel
    restart (evt) {
      if (!this.$el.contains(document.activeElement)) {
        this.start()
      }
    },
    // Update slide list
    updateSlides () {
      this.pause()
      // Get all slides as DOM elements
      this.slides = selectAll('.carousel-item', this.$refs.inner)
      const numSlides = this.slides.length
      // Keep slide number in range
      const index = Math.max(0, Math.min(Math.floor(this.index), numSlides - 1))
      this.slides.forEach((slide, idx) => {
        const n = idx + 1
        if (idx === index) {
          addClass(slide, 'active')
        } else {
          removeClass(slide, 'active')
        }
        setAttr(slide, 'aria-current', idx === index ? 'true' : 'false')
        setAttr(slide, 'aria-posinset', String(n))
        setAttr(slide, 'aria-setsize', String(numSlides))
        slide.tabIndex = -1
      })
      // Set slide as active
      this.setSlide(index)
      this.start()
    },
    calcDirection (direction = null, curIndex = 0, nextIndex = 0) {
      if (!direction) {
        return (nextIndex > curIndex) ? DIRECTION.next : DIRECTION.prev
      }
      return DIRECTION[direction]
    }
  },
  watch: {
    value (newVal, oldVal) {
      if (newVal !== oldVal) {
        this.setSlide(newVal)
      }
    },
    interval (newVal, oldVal) {
      if (newVal === oldVal) {
        return
      }
      if (!newVal) {
        // Pausing slide show
        this.pause()
      } else {
        // Restarting or Changing interval
        this.pause()
        this.start()
      }
    },
    index (val, oldVal) {
      if (val === oldVal || this.isSliding) {
        return
      }
      // Determine sliding direction
      let direction = this.calcDirection(this.direction, oldVal, val)
      // Determine current and next slides
      const currentSlide = this.slides[oldVal]
      const nextSlide = this.slides[val]
      // Don't do anything if there aren't any slides to slide to
      if (!currentSlide || !nextSlide) {
        return
      }
      // Start animating
      this.isSliding = true
      this.$emit('sliding-start', val)
      // Update v-model
      this.$emit('input', this.index)
      nextSlide.classList.add(direction.overlayClass)
      // Trigger a reflow of next slide
      reflow(nextSlide)
      addClass(currentSlide, direction.dirClass)
      addClass(nextSlide, direction.dirClass)
      // Transition End handler
      let called = false
      /* istanbul ignore next: dificult to test */
      const onceTransEnd = (evt) => {
        if (called) {
          return
        }
        called = true
        if (this.transitionEndEvent) {
          const events = this.transitionEndEvent.split(/\s+/)
          events.forEach(event => {
            eventOff(currentSlide, event, onceTransEnd)
          })
        }
        this._animationTimeout = null
        removeClass(nextSlide, direction.dirClass)
        removeClass(nextSlide, direction.overlayClass)
        addClass(nextSlide, 'active')
        removeClass(currentSlide, 'active')
        removeClass(currentSlide, direction.dirClass)
        removeClass(currentSlide, direction.overlayClass)
        setAttr(currentSlide, 'aria-current', 'false')
        setAttr(nextSlide, 'aria-current', 'true')
        setAttr(currentSlide, 'aria-hidden', 'true')
        setAttr(nextSlide, 'aria-hidden', 'false')
        currentSlide.tabIndex = -1
        nextSlide.tabIndex = -1
        if (!this.isCycling) {
          // Focus the next slide for screen readers if not in play mode
          nextSlide.tabIndex = 0
          this.$nextTick(() => {
            nextSlide.focus()
          })
        }
        this.isSliding = false
        this.direction = null
        // Notify ourselves that we're done sliding (slid)
        this.$nextTick(() => this.$emit('sliding-end', val))
      }
      // Clear transition classes after transition ends
      if (this.transitionEndEvent) {
        const events = this.transitionEndEvent.split(/\s+/)
        events.forEach(event => {
          eventOn(currentSlide, event, onceTransEnd)
        })
      }
      // Fallback to setTimeout
      this._animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION)
    }
  },
  created () {
    // Create private non-reactive props
    this._animationTimeout = null
  },
  mounted () {
    // Cache current browser transitionend event name
    this.transitionEndEvent = getTransisionEndEvent(this.$el) || null
    // Get all slides
    this.updateSlides()
    // Observe child changes so we can update slide list
    observeDom(this.$refs.inner, this.updateSlides.bind(this), {
      subtree: false,
      childList: true,
      attributes: true,
      attributeFilter: [ 'id' ]
    })
  },
  /* istanbul ignore next: dificult to test */
  beforeDestroy () {
    clearInterval(this.intervalId)
    clearTimeout(this._animationTimeout)
    this.intervalId = null
    this._animationTimeout = null
  }
}
<commit_msg>fix t<commit_after>import observeDom from '../../utils/observe-dom'
import KeyCodes from '../../utils/key-codes'
import { selectAll, reflow, addClass, removeClass, setAttr, eventOn, eventOff } from '../../utils/dom'
import idMixin from '../../mixins/id'

// Slide directional classes
const DIRECTION = {
  next: {
    dirClass: 'carousel-item-left',
    overlayClass: 'carousel-item-next'
  },
  prev: {
    dirClass: 'carousel-item-right',
    overlayClass: 'carousel-item-prev'
  }
}

// Fallback Transition duration (with a little buffer) in ms
const TRANS_DURATION = 600 + 50

// Transition Event names
const TransitionEndEvents = {
  WebkitTransition: 'webkitTransitionEnd',
  MozTransition: 'transitionend',
  OTransition: 'otransitionend oTransitionEnd',
  transition: 'transitionend'
}

// Return the browser specific transitionEnd event name
function getTransisionEndEvent (el) {
  for (const name in TransitionEndEvents) {
    if (el.style[name] !== undefined) {
      return TransitionEndEvents[name]
    }
  }
  // fallback
  return null
}

export default {
  mixins: [ idMixin ],
  render (h) {
    // Wrapper for slides
    const inner = h(
      'div',
      {
        ref: 'inner',
        class: [ 'carousel-inner' ],
        attrs: {
          id: this.safeId('__BV_inner_'),
          role: 'list'
        }
      },
      [ this.$slots.default ]
    )

    // Prev and Next Controls
    let controls = h(false)
    if (this.controls) {
      controls = [
        h(
          'a',
          {
            class: [ 'carousel-control-prev' ],
            attrs: { href: '#', role: 'button', 'aria-controls': this.safeId('__BV_inner_') },
            on: {
              click: (evt) => {
                evt.preventDefault()
                evt.stopPropagation()
                this.prev()
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.prev()
                }
              }
            }
          },
          [
            h('span', { class: [ 'carousel-control-prev-icon' ], attrs: { 'aria-hidden': 'true' } }),
            h('span', { class: [ 'sr-only' ] }, [ this.labelPrev ])
          ]
        ),
        h(
          'a',
          {
            class: [ 'carousel-control-next' ],
            attrs: { href: '#', role: 'button', 'aria-controls': this.safeId('__BV_inner_') },
            on: {
              click: (evt) => {
                evt.preventDefault()
                evt.stopPropagation()
                this.next()
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.next()
                }
              }
            }
          },
          [
            h('span', { class: [ 'carousel-control-next-icon' ], attrs: { 'aria-hidden': 'true' } }),
            h('span', { class: [ 'sr-only' ] }, [ this.labelNext ])
          ]
        )
      ]
    }

    // Indicators
    const indicators = h(
      'ol',
      {
        class: [ 'carousel-indicators' ],
        directives: [
          { name: 'show', rawName: 'v-show', value: this.indicators, expression: 'indicators' }
        ],
        attrs: {
          id: this.safeId('__BV_indicators_'),
          'aria-hidden': this.indicators ? 'false' : 'true',
          'aria-label': this.labelIndicators,
          'aria-owns': this.safeId('__BV_inner_')
        }
      },
      this.slides.map((slide, n) => {
        return h(
          'li',
          {
            key: `slide_${n}`,
            class: { active: n === this.index },
            attrs: {
              role: 'button',
              id: this.safeId(`__BV_indicator_${n + 1}_`),
              tabindex: this.indicators ? '0' : '-1',
              'aria-current': n === this.index ? 'true' : 'false',
              'aria-label': `${this.labelGotoSlide} ${n + 1}`,
              'aria-describedby': this.slides[n].id || null,
              'aria-controls': this.safeId('__BV_inner_')
            },
            on: {
              click: (evt) => {
                this.setSlide(n)
              },
              keydown: (evt) => {
                const keyCode = evt.keyCode
                if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) {
                  evt.preventDefault()
                  evt.stopPropagation()
                  this.setSlide(n)
                }
              }
            }
          }
        )
      })
    )

    // Return the carousel
    return h(
      'div',
      {
        class: [ 'carousel', 'slide' ],
        style: { background: this.background },
        attrs: {
          role: 'region',
          id: this.safeId(),
          'aria-busy': this.isSliding ? 'true' : 'false'
        },
        on: {
          mouseenter: this.pause,
          mouseleave: this.restart,
          focusin: this.pause,
          focusout: this.restart,
          keydown: (evt) => {
            const keyCode = evt.keyCode
            if (keyCode === KeyCodes.LEFT || keyCode === KeyCodes.RIGHT) {
              evt.preventDefault()
              evt.stopPropagation()
              this[keyCode === KeyCodes.LEFT ? 'prev' : 'next']()
            }
          }
        }
      },
      [ inner, controls, indicators ]
    )
  },
  data () {
    return {
      index: this.value || 0,
      isSliding: false,
      intervalId: null,
      transitionEndEvent: null,
      slides: [],
      direction: null
    }
  },
  props: {
    labelPrev: {
      type: String,
      default: 'Previous Slide'
    },
    labelNext: {
      type: String,
      default: 'Next Slide'
    },
    labelGotoSlide: {
      type: String,
      default: 'Goto Slide'
    },
    labelIndicators: {
      type: String,
      default: 'Select a slide to display'
    },
    interval: {
      type: Number,
      default: 5000
    },
    indicators: {
      type: Boolean,
      default: false
    },
    controls: {
      type: Boolean,
      default: false
    },
    imgWidth: {
      // Sniffed by carousel-slide
      type: [Number, String]
    },
    imgHeight: {
      // Sniffed by carousel-slide
      type: [Number, String]
    },
    background: {
      type: String
    },
    value: {
      type: Number,
      default: 0
    }
  },
  computed: {
    isCycling () {
      return Boolean(this.intervalId)
    }
  },
  methods: {
    // Set slide
    setSlide (slide) {
      // Don't animate when page is not visible
      if (typeof document !== 'undefined' && document.visibilityState && document.hidden) {
        return
      }
      const len = this.slides.length
      // Don't do anything if nothing to slide to
      if (len === 0) {
        return
      }
      // Don't change slide while transitioning, wait until transition is done
      if (this.isSliding) {
        // Schedule slide after sliding complete
        this.$once('sliding-end', () => this.setSlide(slide))
        return
      }
      // Make sure we have an integer (you never know!)
      slide = Math.floor(slide)
      // Set new slide index. Wrap around if necessary
      this.index = slide >= len ? 0 : (slide >= 0 ? slide : len - 1)
    },
    // Previous slide
    prev () {
      this.direction = 'prev'
      this.setSlide(this.index - 1)
    },
    // Next slide
    next () {
      this.direction = 'next'
      this.setSlide(this.index + 1)
    },
    // Pause auto rotation
    pause () {
      if (this.isCycling) {
        clearInterval(this.intervalId)
        this.intervalId = null
        if (this.slides[this.index]) {
          // Make current slide focusable for screen readers
          this.slides[this.index].tabIndex = 0
        }
      }
    },
    // Start auto rotate slides
    start () {
      // Don't start if no interval, or if we are already running
      if (!this.interval || this.isCycling) {
        return
      }
      this.slides.forEach(slide => {
        slide.tabIndex = -1
      })
      this.intervalId = setInterval(() => {
        this.next()
      }, Math.max(1000, this.interval))
    },
    // Re-Start auto rotate slides when focus/hover leaves the carousel
    restart (evt) {
      if (!this.$el.contains(document.activeElement)) {
        this.start()
      }
    },
    // Update slide list
    updateSlides () {
      this.pause()
      // Get all slides as DOM elements
      this.slides = selectAll('.carousel-item', this.$refs.inner)
      const numSlides = this.slides.length
      // Keep slide number in range
      const index = Math.max(0, Math.min(Math.floor(this.index), numSlides - 1))
      this.slides.forEach((slide, idx) => {
        const n = idx + 1
        if (idx === index) {
          addClass(slide, 'active')
        } else {
          removeClass(slide, 'active')
        }
        setAttr(slide, 'aria-current', idx === index ? 'true' : 'false')
        setAttr(slide, 'aria-posinset', String(n))
        setAttr(slide, 'aria-setsize', String(numSlides))
        slide.tabIndex = -1
      })
      // Set slide as active
      this.setSlide(index)
      this.start()
    },
    calcDirection (direction = null, curIndex = 0, nextIndex = 0) {
      if (!direction) {
        return (nextIndex > curIndex) ? DIRECTION.next : DIRECTION.prev
      }
      return DIRECTION[direction]
    }
  },
  watch: {
    value (newVal, oldVal) {
      if (newVal !== oldVal) {
        this.setSlide(newVal)
      }
    },
    interval (newVal, oldVal) {
      if (newVal === oldVal) {
        return
      }
      if (!newVal) {
        // Pausing slide show
        this.pause()
      } else {
        // Restarting or Changing interval
        this.pause()
        this.start()
      }
    },
    index (val, oldVal) {
      if (val === oldVal || this.isSliding) {
        return
      }
      // Determine sliding direction
      let direction = this.calcDirection(this.direction, oldVal, val)
      // Determine current and next slides
      const currentSlide = this.slides[oldVal]
      const nextSlide = this.slides[val]
      // Don't do anything if there aren't any slides to slide to
      if (!currentSlide || !nextSlide) {
        return
      }
      // Start animating
      this.isSliding = true
      this.$emit('sliding-start', val)
      // Update v-model
      this.$emit('input', this.index)
      nextSlide.classList.add(direction.overlayClass)
      // Trigger a reflow of next slide
      reflow(nextSlide)
      addClass(currentSlide, direction.dirClass)
      addClass(nextSlide, direction.dirClass)
      // Transition End handler
      let called = false
      /* istanbul ignore next: dificult to test */
      const onceTransEnd = (evt) => {
        if (called) {
          return
        }
        called = true
        if (this.transitionEndEvent) {
          const events = this.transitionEndEvent.split(/\s+/)
          events.forEach(event => {
            eventOff(currentSlide, event, onceTransEnd)
          })
        }
        this._animationTimeout = null
        removeClass(nextSlide, direction.dirClass)
        removeClass(nextSlide, direction.overlayClass)
        addClass(nextSlide, 'active')
        removeClass(currentSlide, 'active')
        removeClass(currentSlide, direction.dirClass)
        removeClass(currentSlide, direction.overlayClass)
        setAttr(currentSlide, 'aria-current', 'false')
        setAttr(nextSlide, 'aria-current', 'true')
        setAttr(currentSlide, 'aria-hidden', 'true')
        setAttr(nextSlide, 'aria-hidden', 'false')
        currentSlide.tabIndex = -1
        nextSlide.tabIndex = -1
        if (!this.isCycling) {
          // Focus the next slide for screen readers if not in play mode
          nextSlide.tabIndex = 0
          this.$nextTick(() => {
            nextSlide.focus()
          })
        }
        this.isSliding = false
        this.direction = null
        // Notify ourselves that we're done sliding (slid)
        this.$nextTick(() => this.$emit('sliding-end', val))
      }
      // Clear transition classes after transition ends
      if (this.transitionEndEvent) {
        const events = this.transitionEndEvent.split(/\s+/)
        events.forEach(event => {
          eventOn(currentSlide, event, onceTransEnd)
        })
      }
      // Fallback to setTimeout
      this._animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION)
    }
  },
  created () {
    // Create private non-reactive props
    this._animationTimeout = null
  },
  mounted () {
    // Cache current browser transitionend event name
    this.transitionEndEvent = getTransisionEndEvent(this.$el) || null
    // Get all slides
    this.updateSlides()
    // Observe child changes so we can update slide list
    observeDom(this.$refs.inner, this.updateSlides.bind(this), {
      subtree: false,
      childList: true,
      attributes: true,
      attributeFilter: [ 'id' ]
    })
  },
  /* istanbul ignore next: dificult to test */
  beforeDestroy () {
    clearInterval(this.intervalId)
    clearTimeout(this._animationTimeout)
    this.intervalId = null
    this._animationTimeout = null
  }
}
<|endoftext|>