<|start_filename|>src/utils.test.js<|end_filename|>
import {
  listCompare,
  removeElement,
} from './utils';

describe('listCompare functional testing', () => {
  test('correct range selection in regular scenario', () => {
    const parent = [1, 2, 3, 4, 5, 6, 7];
    const selectedList = [3, 4];
    const selectingTarget = 6;

    const compareFunc = listCompare(parent);
    expect(
      compareFunc(selectedList, selectingTarget).sort(),
    ).toEqual([3, 4, 5, 6].sort());
  });

  test('return right result in childList length is 1 (forward)', () => {
    const expected = listCompare([2, 3, 4, 5, 6])([3], 6);
    expect(expected).toEqual([3, 4, 5, 6]);
  });

  test('return right result in childList length is more than 1 (forward)', () => {
    const expected = listCompare([2, 3, 4, 5, 6])([2, 3], 6);
    expect(expected).toEqual([2, 3, 4, 5, 6]);
  });

  test('return right result in childList length is 1 (reverse)', () => {
    const expected = listCompare([2, 3, 4, 5, 6])([5], 2);
    expect(expected).toEqual([2, 3, 4, 5]);
  });

  test('return right result in childList length is more than 1 (reverse)', () => {
    const expected = listCompare([2, 3, 4, 5, 6])([5, 6], 3);
    expect(expected).toEqual([3, 4, 5, 6]);
  });
});

describe('listCompare error handling', () => {
  test('throw error when parentList is empty', () => {
    expect(() => {
      listCompare([]);
    }).toThrow();
  });

  test('throw error when childList is empty', () => {
    expect(() => {
      listCompare([1, 2, 3])([], 2);
    }).toThrow();
  });

  test('throw error when target is not in parentList', () => {
    expect(() => {
      listCompare([1, 2, 3])([1, 2], 4);
    }).toThrow();
  });
});

describe('removeElement function', () => {
  const data = [5, 6, 1, 8, 2];
  test('remove a element', () => {
    expect(removeElement(1)(data)).toEqual([5, 1, 8, 2]);
  });

  test('remove first element', () => {
    expect(removeElement(0)(data)).toEqual([6, 1, 8, 2]);
  });

  test('remove last element', () => {
    expect(removeElement(4)(data)).toEqual([5, 6, 1, 8]);
  });

  test('remove element from index not existing in array', () => {
    expect(removeElement(9)(data)).toEqual([5, 6, 1, 8, 2]);
  });
});



<|start_filename|>script/publishGhpage.js<|end_filename|>
const ghpages = require('gh-pages');

ghpages.publish(
  'static',
  {
    message: 'update demo page',
    push: true,
  },
  (error) => {
    console.log('finish');
    console.log(error);
  },
);


<|start_filename|>demo/Block.js<|end_filename|>
import React from 'react';
import PropTypes from 'prop-types';

import {
  Selectable,
} from './components';

const Block = ({ selected, uid, onClick }) => (
  <Selectable
    selected={selected}
    onClick={onClick}
  >
    {uid}
  </Selectable>
);

Block.propTypes = {
  selected: PropTypes.bool,
  uid: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
  ]),
  onClick: PropTypes.func,
};

export default Block;


<|start_filename|>script/demoConfig.js<|end_filename|>
const babel = require('rollup-plugin-babel');
const resolve = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');
const uglify = require('rollup-plugin-uglify');
const replace = require('rollup-plugin-replace');
const builtins = require('rollup-plugin-node-builtins');
const globals = require('rollup-plugin-node-globals');


const isProduction = process.env.NODE_ENV === 'production';
const NODE_ENV = isProduction ? 'production' : 'development';

const inputOptions = {
  input: 'demo/index.js',
  plugins: [
    babel({
      exclude: ['node_modules/**'],
      plugins: ['external-helpers'],
    }),
    resolve({
      jsnext: true,
      main: true,
      preferBuiltins: true,
    }),
    commonjs({
      include: 'node_modules/**',
      namedExports: {
        'node_modules/react/index.js': ['Component', 'PureComponent', 'Children', 'createElement'],
      },
    }),
    replace({ 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) }),
    builtins(),
    globals(),
    isProduction && uglify(),
  ],
};

const outputOptions = {
  file: 'static/bundle.js',
  format: 'iife', // immediately-invoked function expression — suitable for <script> tags
  sourcemap: true,
};

module.exports = {
  inputOptions,
  outputOptions,
};


<|start_filename|>src/SelectableGroup.js<|end_filename|>
import React from 'react';
import PropTypes from 'prop-types';

import {
  listCompare,
  removeElement,
} from './utils';

import {
  ACTION_NAME,
} from './constants';

class SelectableGroup extends React.Component {
  static propTypes = {
    children: PropTypes.node,
    onChange: PropTypes.func.isRequired,
    selectedList: PropTypes.array.isRequired,
    uidList: PropTypes.array.isRequired,
    Component: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.func,
      PropTypes.object,
    ]),
  }

  static defaultProps = {
    Component: 'div',
    selectedList: [],
  }

  static childContextTypes = {
    [ACTION_NAME]: PropTypes.shape({
      oneClick: PropTypes.func,
      toggleClick: PropTypes.func,
      rangeSelect: PropTypes.func,
    }),
  }

  getChildContext() {
    return (
      {
        [ACTION_NAME]: {
          oneClick: this._oneClick,
          toggleClick: this._toggleClick,
          rangeSelect: this._rangeSelect,
        },
      }
    );
  }

  _toggleClick = (uid) => {
    const {
      selectedList: prevSelectedList,
      onChange,
    } = this.props;

    const index = prevSelectedList.indexOf(uid);

    const selectedList = index < 0 ?
      prevSelectedList.concat([uid]) :
      removeElement(index)(prevSelectedList);

    onChange(selectedList);
  }

  _oneClick = (uid) => {
    const {
      selectedList: prevSelectedList,
      onChange,
    } = this.props;

    const index = prevSelectedList.indexOf(uid);

    const isSelected = prevSelectedList.length > 1 || index < 0;
    const selectedList = isSelected ? [uid] : removeElement(index)(prevSelectedList);

    return onChange(selectedList);
  }

  _rangeSelect = (uid) => {
    const {
      selectedList,
      uidList,
      onChange,
    } = this.props;
    if (selectedList.length === 0) return null;

    const allCompare = listCompare(uidList);

    return onChange(allCompare(selectedList, uid));
  }


  render() {
    const {
      Component,
      children,
      selectedList,
      uidList,
      onChange,
      ...rest
    } = this.props;

    return (
      <Component
        {...rest}
      >
        {children}
      </Component>
    );
  }
}


export default SelectableGroup;


<|start_filename|>src/index.js<|end_filename|>
import SelectableGroup from './SelectableGroup';
import createSelectable from './createSelectable';

export {
  SelectableGroup,
  // FIXME remove this in v3
  createSelectable as createSeletable,
  createSelectable,
};


<|start_filename|>demo/App.js<|end_filename|>
import React from 'react';

import {
  createSeletable,
  SelectableGroup,
} from '../src';

import Block from './Block';

import {
  Container,
  NonSelectable,
  Result,
} from './components';

const SelectableBlock = createSeletable(Block);

const elements = Array(24).fill(0).map((_, index) => (
  {
    id: index,
  }
));


class App extends React.Component {
  state = {
    selectedList: [],
  };

  _handleChange = (selectedList) => {
    this.setState({
      selectedList,
    });
  }

  render() {
    const {
      selectedList,
    } = this.state;
    return (
      <Container>
        <div>
          <SelectableGroup
            selectedList={selectedList}
            uidList={elements.map(e => e.id)}
            onChange={this._handleChange}
            style={{
              display: 'flex',
              width: '800px',
              flexWrap: 'wrap',
            }}
          >
            {
              elements.map(ele => (
                <SelectableBlock
                  key={ele.id}
                  uid={ele.id}
                  selected={selectedList.indexOf(ele.id) >= 0}
                />
              ))
            }
            <NonSelectable />
            <NonSelectable />
          </SelectableGroup>
        </div>
        <Result>
          <div>selected: </div>
          <ul>
            {
              this.state.selectedList.map(ele => (
                <li key={ele}>{ele}</li>
              ))
            }
          </ul>
        </Result>
      </Container>
    );
  }
}

export default App;
