├── .github
└── CONTRIBUTING.md
├── .eslintignore
├── .npmignore
├── components
├── utils
│ ├── noop.js
│ ├── lastElement.js
│ ├── getSelectedIds.js
│ ├── autoBind.js
│ └── debounce.js
├── Count
│ ├── index.js
│ ├── Count.scss
│ └── Count.jsx
├── Group
│ ├── index.js
│ └── Group.jsx
├── Slider
│ ├── index.js
│ ├── utils
│ │ ├── isVertical.js
│ │ ├── suppress.js
│ │ ├── capitalize.js
│ │ ├── hasStepDifference.js
│ │ ├── removeClass.js
│ │ ├── isWithinRange.js
│ │ ├── isEqual.js
│ │ ├── index.js
│ │ ├── formatValue.js
│ │ └── formatNumber.js
│ ├── helpers
│ │ ├── getPositionFromValue.js
│ │ ├── index.js
│ │ ├── getValueFromPosition.js
│ │ ├── getRelativePosition.js
│ │ └── getNearestValue.js
│ ├── constants.js
│ ├── Rail.js
│ ├── Steps.js
│ ├── Slider.scss
│ ├── Control.js
│ └── Slider.jsx
├── Toggle
│ ├── index.js
│ ├── Toggle.scss
│ └── Toggle.jsx
├── InputRange
│ ├── index.js
│ ├── InputRange.scss
│ └── InputRange.jsx
├── AutoComplete
│ ├── index.js
│ ├── Suggestions.js
│ ├── Tag.js
│ ├── AutoComplete.scss
│ ├── SearchBox.js
│ └── AutoComplete.jsx
├── styles.scss
├── mixins.scss
├── index.js
├── theme.scss
└── icons.scss
├── assets
├── logo.png
└── fonts
│ ├── reactfilters.eot
│ ├── reactfilters.ttf
│ ├── reactfilters.woff
│ └── reactfilters.svg
├── .babelrc
├── docs
├── cd369ac486335815aba6faf43e3476e9.eot
├── eefe50b894a27050a86bd6ae7a822682.ttf
├── static
│ ├── preview.bundle.js.map
│ └── manager.bundle.js.map
├── iframe.html
├── index.html
└── 0ab516c6e00c001e01c647f19720b2e8.svg
├── .gitignore
├── test
├── Slider
│ └── utils
│ │ ├── capitalize.test.js
│ │ ├── removeClass.test.js
│ │ ├── isEqual.test.js
│ │ ├── hasStepDifference.test.js
│ │ ├── formatNumber.test.js
│ │ └── isWithinRange.test.js
├── utils
│ ├── lastElement.test.js
│ └── debounce.test.js
├── config
│ └── setup.js
├── Count.test.js
├── Group.test.js
└── Toggle.test.js
├── .storybook
├── base.scss
├── webpack.config.js
└── config.js
├── .travis.yml
├── stories
├── CheckBox.story.js
├── InputRange.story.js
├── Count.story.js
├── Switch.story.js
├── Slider.value.story.js
├── Radio.story.js
├── Container.js
├── Slider.range.story.js
├── Group.story.js
└── AutoComplete.story.js
├── mdDocs
├── InputRange.md
├── Group.md
├── Count.md
├── Slider.md
├── AutoComplete.md
└── Toggle.md
├── .eslintrc
├── .editorconfig
├── LICENSE.md
├── scripts
├── buildDocs.js
└── generateMarkdown.js
├── README.md
├── package.json
└── CHANGELOG.md
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | **/node_modules
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | tests
4 | coverage
5 |
--------------------------------------------------------------------------------
/components/utils/noop.js:
--------------------------------------------------------------------------------
1 | export default function () {}
2 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/components/Count/index.js:
--------------------------------------------------------------------------------
1 | import Count from './Count';
2 | export default Count;
3 |
--------------------------------------------------------------------------------
/components/Group/index.js:
--------------------------------------------------------------------------------
1 | import Group from './Group';
2 | export default Group;
3 |
--------------------------------------------------------------------------------
/components/Slider/index.js:
--------------------------------------------------------------------------------
1 | import Slider from './Slider';
2 | export default Slider;
3 |
--------------------------------------------------------------------------------
/components/Toggle/index.js:
--------------------------------------------------------------------------------
1 | import Toggle from './Toggle';
2 | export default Toggle;
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/components/InputRange/index.js:
--------------------------------------------------------------------------------
1 | import InputRange from './InputRange';
2 | export default InputRange;
3 |
--------------------------------------------------------------------------------
/components/AutoComplete/index.js:
--------------------------------------------------------------------------------
1 | import AutoComplete from './AutoComplete';
2 | export default AutoComplete;
3 |
--------------------------------------------------------------------------------
/assets/fonts/reactfilters.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.eot
--------------------------------------------------------------------------------
/assets/fonts/reactfilters.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.ttf
--------------------------------------------------------------------------------
/assets/fonts/reactfilters.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.woff
--------------------------------------------------------------------------------
/components/Slider/utils/isVertical.js:
--------------------------------------------------------------------------------
1 | export default function (orientation) {
2 | return orientation === 'vertical';
3 | }
4 |
--------------------------------------------------------------------------------
/components/Slider/utils/suppress.js:
--------------------------------------------------------------------------------
1 | export default function (e) {
2 | e.stopPropagation();
3 | e.preventDefault();
4 | }
5 |
--------------------------------------------------------------------------------
/components/Slider/utils/capitalize.js:
--------------------------------------------------------------------------------
1 | export default function (str) {
2 | return str.charAt(0).toUpperCase() + str.substr(1);
3 | }
4 |
--------------------------------------------------------------------------------
/docs/cd369ac486335815aba6faf43e3476e9.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/docs/cd369ac486335815aba6faf43e3476e9.eot
--------------------------------------------------------------------------------
/docs/eefe50b894a27050a86bd6ae7a822682.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritz078/react-filters/HEAD/docs/eefe50b894a27050a86bd6ae7a822682.ttf
--------------------------------------------------------------------------------
/components/Slider/utils/hasStepDifference.js:
--------------------------------------------------------------------------------
1 | export default function (newValue, oldValue, step) {
2 | return Math.abs(newValue - oldValue) >= step;
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 | .idea
8 | examples/bundle.js
9 | examples/style.css
10 | npm-debug.log
11 |
--------------------------------------------------------------------------------
/components/Slider/helpers/getPositionFromValue.js:
--------------------------------------------------------------------------------
1 | export default function (props) {
2 | const { min, max, value } = props;
3 | return ((value / (max - min))) * 100;
4 | }
5 |
--------------------------------------------------------------------------------
/components/utils/lastElement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * returns the last element of the array
3 | * @param arr
4 | */
5 | export default function (arr) {
6 | return arr.length ? arr[arr.length - 1] : null;
7 | }
8 |
--------------------------------------------------------------------------------
/components/styles.scss:
--------------------------------------------------------------------------------
1 | @import "./icons";
2 | @import "./Count/Count";
3 | @import "./Toggle/Toggle";
4 | @import "./Slider/Slider";
5 | @import "./AutoComplete/AutoComplete";
6 | @import "./InputRange/InputRange";
7 |
8 |
--------------------------------------------------------------------------------
/components/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin no-select {
2 | -webkit-touch-callout: none;
3 | -webkit-user-select: none;
4 | -khtml-user-select: none;
5 | -moz-user-select: none;
6 | -ms-user-select: none;
7 | user-select: none;
8 | }
9 |
--------------------------------------------------------------------------------
/docs/static/preview.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"static/preview.bundle.js","sources":["webpack:///static/preview.bundle.js","webpack:///"],"mappings":"AAAA;ACynIA;AAopIA;AA0zIA;AAk8EA;AAs5EA;AA+4GA;AA4gFA;AA4nIA;AAg/GA;AAw4GA;AA2oEA;AA2+DA;AAq4FA","sourceRoot":""}
--------------------------------------------------------------------------------
/docs/static/manager.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"static/manager.bundle.js","sources":["webpack:///static/manager.bundle.js","webpack:///?d41d"],"mappings":"AAAA;AC2kIA;AAu6IA;AAywIA;AA4zEA;AAw1EA;AAsoHA;AAgzFA;AA8wHA;AA0mHA;AAqvHA;AAotFA;AAk9CA;AAqtGA","sourceRoot":""}
--------------------------------------------------------------------------------
/components/Slider/utils/removeClass.js:
--------------------------------------------------------------------------------
1 | export default function (element, className) {
2 | const classes = element.className.split(' ');
3 | const i = classes.indexOf(className);
4 | if (i !== -1) {
5 | classes.splice(i, 1);
6 | }
7 | return classes.join(' ');
8 | }
9 |
--------------------------------------------------------------------------------
/components/Slider/constants.js:
--------------------------------------------------------------------------------
1 | export default {
2 | horizontal: {
3 | coordinate: 'x',
4 | dimension: 'width',
5 | direction: 'left'
6 | },
7 | vertical: {
8 | coordinate: 'y',
9 | dimension: 'height',
10 | direction: 'top'
11 | }
12 | };
13 |
14 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | import AutoComplete from './AutoComplete';
2 | import Count from './Count';
3 | import Slider from './Slider';
4 | import Toggle from './Toggle';
5 | import InputRange from './InputRange';
6 | import Group from './Group';
7 |
8 | export { AutoComplete, Count, Slider, Toggle, InputRange, Group };
9 |
--------------------------------------------------------------------------------
/components/InputRange/InputRange.scss:
--------------------------------------------------------------------------------
1 | .rf-input-range{
2 | width: 300px;
3 |
4 | .rf-autocomplete{
5 | width: 47%;
6 | display: inline-block;
7 | vertical-align: top;
8 | }
9 |
10 | .ir-separator{
11 | width: 6%;
12 | display: inline-block;
13 | vertical-align: top;
14 | text-align: center;
15 | line-height: 30px;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/test/Slider/utils/capitalize.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { capitalize } = require(`${__base}components/Slider/utils/`);
3 | import { expect } from 'chai';
4 |
5 | describe('capitalize util method()', () => {
6 | it('should return capitalized string', () => {
7 | const str = 'foo bar';
8 | expect(capitalize(str)).to.equal('Foo bar');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/Slider/helpers/index.js:
--------------------------------------------------------------------------------
1 | import getPositionFromValue from './getPositionFromValue';
2 | import getValueFromPosition from './getValueFromPosition';
3 | import getRelativePosition from './getRelativePosition';
4 | import getNearestValue from './getNearestValue';
5 |
6 | export {
7 | getPositionFromValue,
8 | getValueFromPosition,
9 | getRelativePosition,
10 | getNearestValue
11 | };
12 |
--------------------------------------------------------------------------------
/components/Slider/utils/isWithinRange.js:
--------------------------------------------------------------------------------
1 | export default function (props, value, position) {
2 | if (position) return position <= props.trackLength;
3 | const { min, max, step } = props;
4 | if (typeof value === 'object') { // if Array
5 | return (value[1] - value[0] >= step) && value[0] >= min && value[1] <= max;
6 | } else {
7 | // TODO : is step needed here ?
8 | return (value >= min && value <= max);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/components/Slider/helpers/getValueFromPosition.js:
--------------------------------------------------------------------------------
1 | import { formatNumber, isVertical } from '../utils';
2 | import constants from '../constants';
3 |
4 | export default function (props, position) {
5 | const { min, max, trackOffset, step, orientation } = props;
6 | const ratio = (max - min) / trackOffset[constants[orientation].dimension];
7 | if (isVertical(orientation)) position = trackOffset.height - position;
8 | return formatNumber(position * ratio, step, min);
9 | }
10 |
--------------------------------------------------------------------------------
/test/utils/lastElement.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | import { expect } from 'chai';
3 |
4 | const lastElement = require(`${__base}components/utils/lastElement`).default;
5 |
6 | describe('lastElement', () => {
7 | it('should return the last element of an Array', () => {
8 | expect(lastElement([1, 2, 3])).to.equal(3);
9 | });
10 |
11 | it('should return null if empty array is passed', () => {
12 | expect(lastElement([])).to.equal(null);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/Slider/utils/removeClass.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { removeClass } = require(`${__base}components/Slider/utils/`);
3 | import { expect } from 'chai';
4 |
5 | describe('removeClass utility method', () => {
6 | it('should remove return the class string after removing the specified string', () => {
7 | const element = {
8 | className: 'foo bar hello'
9 | };
10 |
11 | expect(removeClass(element, 'bar')).to.equal('foo hello');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/.storybook/base.scss:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Lato);
2 | @import "../components/styles";
3 |
4 | body{
5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
6 | }
7 |
8 | .ac-suggestion{
9 | span{
10 | float: right;
11 | }
12 | }
13 |
14 | .range-container{
15 | width: 80%;
16 | }
17 |
18 | .rf-autocomplete{
19 | width: 80%;
20 | }
21 |
22 | .group{
23 | width: 200px;
24 | }
25 |
--------------------------------------------------------------------------------
/components/Slider/utils/isEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tells if two values are exactly similar
3 | * @param val1
4 | * @param val2
5 | * @returns {boolean}
6 | */
7 | export default function (val1, val2) {
8 | let isSame = true;
9 | if (typeof val1 === 'object' && typeof val2 === 'object') {
10 | if (val1.length !== val2.length) return false;
11 | val1.forEach((val, i) => {
12 | if (val2[i] !== val) isSame = false;
13 | });
14 | } else {
15 | isSame = (val1 === val2);
16 | }
17 |
18 | return isSame;
19 | }
20 |
--------------------------------------------------------------------------------
/components/utils/getSelectedIds.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns an array of ids which are selected
3 | * if arr = [{key: 1, value:true}, {key:2, value:false}, {key: 3, value: true}] and key = 'key'
4 | * it will return [1, 3]
5 | * @param arr
6 | * @param key
7 | * @param number whether return number or Array
8 | */
9 | export default function (arr, key, number = false) {
10 | const selected = [];
11 | arr.forEach(val => {
12 | if (val.value) selected.push(val[key]);
13 | });
14 | return number ? selected[0] : selected;
15 | }
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | cache:
4 | directories:
5 | - node_modules
6 | notifications:
7 | email: false
8 | node_js:
9 | - '4'
10 | before_install:
11 | - npm i -g npm@^2.0.0
12 | before_script:
13 | - npm prune
14 | script:
15 | - npm run test:cover
16 | after_success:
17 | - npm run test:report
18 | - npm run semantic-release
19 | branches:
20 | except:
21 | - "/^v\\d+\\.\\d+\\.\\d+$/"
22 | addons:
23 | code_climate:
24 | repo_token: 842be7e8b1dee369a0c5077a36d43ae401a153731331283c570c725148a10fb5
25 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | module: {
5 | loaders: [
6 | {
7 | test: /\.scss$/,
8 | loaders: ["style", "css", "sass"],
9 | include: path.resolve(__dirname, '../')
10 | },
11 | {
12 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
13 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
14 | },
15 | {
16 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
17 | loader: "file-loader"
18 | }
19 | ]
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/components/utils/autoBind.js:
--------------------------------------------------------------------------------
1 | /**
2 | * binds all the methods with the passed context
3 | * Eg : autoBind(['method1', 'method2'], this)
4 | * is same as
5 | * this.method1 = this.method1.bind(this);
6 | * this.method2 = this.method2.bind(this);
7 | *
8 | * @param methods => An array of method names
9 | * @param context => the binding `this` context
10 | */
11 | export default function (methods, context) {
12 | methods.forEach(method => {
13 | // eslint-disable-next-line no-param-reassign
14 | context[method] = context[method].bind(context);
15 | });
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/components/theme.scss:
--------------------------------------------------------------------------------
1 | $primary-color: #6495ED;
2 | $secondary-color: #e4e4e4;
3 | $tertiary-color: darkgrey;
4 |
5 | // Slider
6 | $slider-track-width: 6px;
7 | $slider-control-width: 15px;
8 | $slider-step-width: 6px;
9 |
10 | //Toggle
11 | $toggle-height: 24px;
12 | $toggle-width: 44px;
13 | $toggle-padding: 2px;
14 | $toggle-btn-color: #ffffff;
15 | $toggle-count-color: grey;
16 | $toggle-label-font-size: $toggle-height * (11px / 22px);
17 | $toggle-label-line-height: $toggle-label-font-size * 1.6;
18 |
19 | // Count
20 | $count-width: 120px;
21 | $count-height: 40px;
22 | $count-padding: 4px;
23 |
--------------------------------------------------------------------------------
/test/config/setup.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | const path = require('path');
3 |
4 | const jsdom = require('jsdom').jsdom;
5 | const exposedProperties = ['window', 'navigator', 'document'];
6 |
7 | global.document = jsdom('');
8 | global.window = document.defaultView;
9 | Object.keys(document.defaultView).forEach((property) => {
10 | if (typeof global[property] === 'undefined') {
11 | exposedProperties.push(property);
12 | global[property] = document.defaultView[property];
13 | }
14 | });
15 |
16 | global.navigator = {
17 | userAgent: 'node.js'
18 | };
19 | global.__base = `${path.resolve()}/`;
20 |
--------------------------------------------------------------------------------
/components/Slider/utils/index.js:
--------------------------------------------------------------------------------
1 | import hasStepDifference from './hasStepDifference';
2 | import capitalize from './capitalize';
3 | import formatNumber from './formatNumber';
4 | import suppress from './suppress';
5 | import isWithinRange from './isWithinRange';
6 | import isEqual from './isEqual';
7 | import removeClass from './removeClass';
8 | import isVertical from './isVertical';
9 | import formatValue from './formatValue';
10 |
11 | export {
12 | capitalize,
13 | hasStepDifference,
14 | formatNumber,
15 | suppress,
16 | isWithinRange,
17 | isEqual,
18 | removeClass,
19 | isVertical,
20 | formatValue
21 | };
22 |
--------------------------------------------------------------------------------
/stories/CheckBox.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import { Toggle } from '../components';
4 | import Container from './Container';
5 |
6 | storiesOf('Checkbox Button', module)
7 | .addDecorator((story) => {story()})
8 | .add('with a text', () => (
9 |
13 | ))
14 | .add('with no text', () => (
15 |
22 | ));
23 |
--------------------------------------------------------------------------------
/test/Slider/utils/isEqual.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { isEqual } = require(`${__base}components/Slider/utils/`);
3 | import { expect } from 'chai';
4 |
5 | describe('isArrayEqual Method', () => {
6 | it('should return true if two arrays are exactly same', () => {
7 | const array1 = [2, 'hello'];
8 | const array2 = [2, 'hello'];
9 | expect(isEqual(array1, array2)).to.equal(true);
10 |
11 | const array3 = ['2', 'hello'];
12 | expect(isEqual(array1, array3)).to.equal(false);
13 |
14 | const array4 = [2, 'hello', 2];
15 | expect(isEqual(array1, array4)).to.equal(false);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/stories/InputRange.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@kadira/storybook';
3 | import { InputRange } from '../components';
4 |
5 | const list = [{
6 | id: 1,
7 | title: 'The Great Gatsby',
8 | author: 'F. Scott Fitzgerald'
9 | }, {
10 | id: 2,
11 | title: 'The DaVinci Code',
12 | author: 'Dan Brown'
13 | }, {
14 | id: 3,
15 | title: 'Angels & Demons',
16 | author: 'Dan Brown'
17 | }];
18 |
19 | const suggestions = [list, list];
20 |
21 | storiesOf('InputRange', module)
22 | .add('Basic', () => (
23 |
27 | ));
28 |
--------------------------------------------------------------------------------
/test/Slider/utils/hasStepDifference.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { hasStepDifference } = require(__base + 'components/Slider/utils/');
3 | import { expect } from 'chai';
4 |
5 | describe('hasStepDifference method', () => {
6 | it('should tell whether the lower and upper value difference is multiple of steps or > step',
7 | () => {
8 | const oldValue = 6;
9 | const newValue = 9;
10 | expect(hasStepDifference(newValue, oldValue, 2)).to.equal(true);
11 | expect(hasStepDifference(newValue, oldValue, 3)).to.equal(true);
12 | expect(hasStepDifference(2, 1, 2)).to.equal(false);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/docs/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 | React Storybook
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/stories/Count.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 |
4 | import { Count } from '../components';
5 | import Container from './Container';
6 |
7 | storiesOf('Count', module)
8 | .addDecorator((story) => {story()})
9 | .add('Basic', () => (
10 |
11 | ))
12 | .add('Disabled', () => (
13 |
14 | ))
15 | .add('Define Range', () => (
16 |
17 | ))
18 | .add('Stepped change', () => (
19 |
20 | ));
21 |
--------------------------------------------------------------------------------
/test/Slider/utils/formatNumber.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { formatNumber } = require(`${__base}components/Slider/utils/`);
3 | import { expect } from 'chai';
4 |
5 | describe('formatNumber', () => {
6 | it('should return nearest valid integer if min = 0', () => {
7 | expect(formatNumber(24.6, 1, 0)).to.equal(25);
8 | expect(formatNumber(24.6, 2, 0)).to.equal(24);
9 | expect(formatNumber(24.8, 3, 0)).to.equal(24);
10 | });
11 |
12 | it('should return nearest valid integer if min != 0', () => {
13 | expect(formatNumber(25.8796, 2, 1)).to.equal(25);
14 | expect(formatNumber(25.4, 3, 2)).to.equal(26);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/stories/Switch.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import { Toggle } from '../components';
4 | import Container from './Container';
5 |
6 | storiesOf('Switch Button')
7 | .addDecorator((story) => {story()})
8 | .add('Basic', () => (
9 |
10 | ))
11 | .add('Label and count', () => (
12 |
17 | ))
18 | .add('Icon Label', () => (
19 |
23 | ));
24 |
--------------------------------------------------------------------------------
/components/Slider/utils/formatValue.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the new value Array or string based on which control is changed and
3 | * type of slider
4 | * @param currentVal : current value of the slider (Array | Number)
5 | * @param newValue : value of one control (Number)
6 | * @param changed : 'lower' or 'upper'
7 | * @param type : 'range' or 'value'
8 | * @returns {*}
9 | */
10 | export default function (currentVal, newValue, changed, type) {
11 | let val = newValue;
12 | if (type === 'range') {
13 | if (typeof newValue === 'object') return newValue;
14 | val = (changed === 'lower') ? [newValue, currentVal[1]] : [currentVal[0], newValue];
15 | }
16 | return val;
17 | }
18 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, addDecorator } from '@kadira/storybook';
2 | import './base.scss';
3 | import centered from '@kadira/react-storybook-decorator-centered';
4 |
5 | addDecorator(centered);
6 |
7 | function loadStories () {
8 | require('../stories/Switch.story');
9 | require('../stories/Radio.story');
10 | require('../stories/Slider.range.story.js');
11 | require('../stories/CheckBox.story');
12 | require('../stories/AutoComplete.story');
13 | require('../stories/Count.story');
14 | require('../stories/InputRange.story');
15 | require('../stories/Group.story');
16 | require('../stories/Slider.value.story.js');
17 | }
18 |
19 | configure(loadStories, module);
20 |
--------------------------------------------------------------------------------
/mdDocs/InputRange.md:
--------------------------------------------------------------------------------
1 | `InputRange` (component)
2 | ========================
3 |
4 |
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [disabled](#disabled)| |`bool`
12 | [name](#name)| |`string`
13 | [onSelect](#onselect)|✔️|`func`
14 | [placeholders](#placeholders)| |`arrayOf[object Object]`
15 | [suggestions](#suggestions)| |`array`
16 | ### `disabled`
17 | type: `bool`
18 | defaultValue: `false`
19 |
20 |
21 |
22 | ### `name`
23 | type: `string`
24 |
25 |
26 |
27 | ### `onSelect` (required)
28 | type: `func`
29 | defaultValue: `noop`
30 |
31 |
32 |
33 | ### `placeholders`
34 | type: `arrayOf[object Object]`
35 |
36 |
37 |
38 | ### `suggestions`
39 | type: `array`
40 |
41 |
42 |
--------------------------------------------------------------------------------
/components/utils/debounce.js:
--------------------------------------------------------------------------------
1 | // Returns a function, that, as long as it continues to be invoked, will not
2 | // be triggered. The function will be called after it stops being called for
3 | // N milliseconds. If `immediate` is passed, trigger the function on the
4 | // leading edge, instead of the trailing.
5 | export default function (func, wait, immediate) {
6 | let timeout;
7 | return function ({ ...args }) {
8 | const context = this;
9 | const later = () => {
10 | timeout = null;
11 | if (!immediate) func.apply(context, args);
12 | };
13 | const callNow = immediate && !timeout;
14 | clearTimeout(timeout);
15 | timeout = setTimeout(later, wait);
16 | if (callNow) func.apply(context, args);
17 | };
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/components/Slider/utils/formatNumber.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the closest integer value to the value calculated by cursor position
3 | * @param value non-formatted value calculated by cursor position
4 | * @param step step difference allowed in the slider
5 | * @param min minimum value on the slider
6 | * @returns {*} the nearest rounded valid number
7 | * eg: if value = 24.6 and step = 2 the closest valid number is 24 and not 25 since
8 | * the next valid number is 26 which is farther than 24.
9 | */
10 | export default function (value, step, min) {
11 | const remainder = value % step;
12 | const prevNumber = (value - remainder) + min;
13 | const nextNumber = prevNumber + step;
14 | return (value - prevNumber) >= (nextNumber - value) ? nextNumber : prevNumber;
15 | }
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "mocha": true
8 | },
9 | "plugins": [
10 | "react"
11 | ],
12 | "globals": {
13 | "jest": false
14 | },
15 | "ecmaFeatures": {
16 | "jsx": true,
17 | "globalReturn": false
18 | },
19 | "rules": {
20 | "comma-dangle" : [1, "never"],
21 | "func-names" : 0,
22 | "key-spacing" : 0,
23 | "space-before-function-paren" : [2, "always"],
24 | "no-else-return" : 0,
25 | "no-multi-spaces" : 0,
26 | "quotes" : [2, "single"],
27 | "jsx-quotes" : [2, "prefer-single"],
28 | "one-var" : 0,
29 | "react/prefer-template" : 0,
30 | "no-underscore-dangle" : 0
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/Slider/utils/isWithinRange.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, __base } = global;
2 | const { isWithinRange } = require(`${__base}components/Slider/utils/`);
3 | import { expect } from 'chai';
4 |
5 | describe('isWithinRange utility method', () => {
6 | const props = {
7 | trackLength : 400,
8 | min: 5,
9 | max: 15,
10 | step: 3
11 | };
12 |
13 | it('should tell if the value is within range for a number', () => {
14 | expect(isWithinRange(props, 8)).to.equal(true);
15 | expect(isWithinRange(props, 16)).to.equal(false);
16 | expect(isWithinRange(props, 4)).to.equal(false);
17 | });
18 |
19 | it('should tell if the values are withing range and following steps for Array', () => {
20 | expect(isWithinRange(props, [8, 11])).to.equal(true);
21 | expect(isWithinRange(props, [4, 14])).to.equal(false);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/components/AutoComplete/Suggestions.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function Suggestions (props) {
5 | const resultsTemplate = props.results.map((val, i) =>
6 | props.resultsTemplate(val, i, props.selectedIndex));
7 | return (
8 |
9 | {resultsTemplate}
10 |
11 | );
12 | }
13 |
14 | Suggestions.defaultResultsTemplate = function (val, i, selectedIndex) {
15 | const className = classNames('ac-suggestion', {
16 | 'ac-suggestion-active': i === selectedIndex
17 | });
18 | return {val.title}
;
19 | };
20 |
21 | Suggestions.propTypes = {
22 | results: PropTypes.array.isRequired,
23 | resultsTemplate: PropTypes.func,
24 | selectedIndex: PropTypes.number
25 | };
26 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | [*]
8 | # Indentation style
9 | # Possible values - tab, space
10 | indent_style = space
11 |
12 | # Indentation size in single-spaced characters
13 | # Possible values - an integer, tab
14 | indent_size = 2
15 |
16 | # Line ending file format
17 | # Possible values - lf, crlf, cr
18 | end_of_line = lf
19 |
20 | # File character encoding
21 | # Possible values - latin1, utf-8, utf-16be, utf-16le
22 | charset = utf-8
23 |
24 | # Denotes whether to trim whitespace at the end of lines
25 | # Possible values - true, false
26 | trim_trailing_whitespace = true
27 |
28 | # Denotes whether file should end with a newline
29 | # Possible values - true, false
30 | insert_final_newline = true
31 |
--------------------------------------------------------------------------------
/components/Count/Count.scss:
--------------------------------------------------------------------------------
1 | @import "../theme";
2 |
3 | .rf-count {
4 | width: $count-width;
5 | background-color: #f6f6f6;
6 | padding: $count-padding;
7 | overflow: auto;
8 |
9 | &.disabled{
10 | opacity: 0.8;
11 | pointer-events: none;
12 | }
13 | }
14 |
15 | .count-button {
16 | border: 1px solid #e5e5e5;
17 | background-color: #ffffff;
18 | outline: none;
19 | border-radius: 2px;
20 | padding: 8px 10px;
21 | color: $primary-color;
22 | display: inline-block;
23 | vertical-align: top;
24 | float: left;
25 | height: $count-height - 2*$count-padding;
26 | width: $count-height - 2*$count-padding;
27 | cursor: pointer;
28 | }
29 |
30 | .count-value {
31 | display: inline-block;
32 | vertical-align: top;
33 | line-height: 32px;
34 | float: left;
35 | overflow: hidden;
36 | width: $count-width - 2*($count-height - 2*$count-padding);
37 | text-align: center;
38 | }
39 |
--------------------------------------------------------------------------------
/components/Slider/helpers/getRelativePosition.js:
--------------------------------------------------------------------------------
1 | import { capitalize } from '../utils';
2 | import constants from '../constants';
3 |
4 | export default function (e, props, sliderWidth) {
5 | // Get the offset DIRECTION relative to the viewport
6 |
7 | const coordinate = constants[props.orientation].coordinate;
8 | const direction = constants[props.orientation].direction;
9 | const ucCoordinate = capitalize(coordinate);
10 | const trackPos = props.trackOffset[direction];
11 |
12 | let btnPos = 0;
13 |
14 | if (typeof e[`page${ucCoordinate}`] !== 'undefined') {
15 | btnPos = e[`page${ucCoordinate}`];
16 | } else if (e && typeof e[`client${ucCoordinate}`] !== 'undefined') {
17 | btnPos = e[`client${ucCoordinate}`];
18 | } else if (e.touches && e.touches[0] &&
19 | typeof e.touches[0][`client${ucCoordinate}`] !== 'undefined') {
20 | btnPos = e.touches[0][`client${ucCoordinate}`];
21 | }
22 |
23 | return btnPos - trackPos - sliderWidth / 2;
24 | }
25 |
--------------------------------------------------------------------------------
/stories/Slider.value.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import { Slider } from '../components';
4 | import Container from './Container';
5 |
6 | storiesOf('Slider Component (Value)', module)
7 | .addDecorator((story) => (
8 |
13 | {story()}
14 | ))
15 | .add('Default', () => (
16 |
17 | ))
18 | .add('Read only', () => (
19 |
20 | ))
21 | .add('Disabled', () => (
22 |
23 | ))
24 | .add('Steps', () => (
25 |
26 | ))
27 | .add('Vertical', () => (
28 |
35 | ));
36 |
--------------------------------------------------------------------------------
/stories/Radio.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import { Toggle } from '../components';
4 | import Container from './Container';
5 |
6 | storiesOf('Radio Button', module)
7 | .addDecorator((story) => {story()})
8 | .add('Default', () => (
9 |
14 | ))
15 | .add('Disabled', () => (
16 |
23 | ))
24 | .add('Tag', () => (
25 |
32 | ))
33 | .add('Tag Disabled', () => (
34 |
42 | ));
43 |
--------------------------------------------------------------------------------
/mdDocs/Group.md:
--------------------------------------------------------------------------------
1 | `Group` (component)
2 | ===================
3 |
4 |
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [attributes](#attributes)| |`object`
12 | [className](#classname)| |`string`
13 | [id](#id)| |`string`
14 | [mode](#mode)| |`enum('normal'|'tag')`
15 | [name](#name)|✔️|`string`
16 | [onChange](#onchange)|✔️|`func`
17 | [selectedIds](#selectedids)| |`array`
18 | [type](#type)| |`enum('radio'|'checkbox'|'switch')`
19 | [value](#value)| |`arrayOf[object Object]`
20 | ### `attributes`
21 | type: `object`
22 |
23 |
24 |
25 | ### `className`
26 | type: `string`
27 |
28 |
29 |
30 | ### `id`
31 | type: `string`
32 | defaultValue: `'id'`
33 |
34 |
35 |
36 | ### `mode`
37 | type: `enum('normal'|'tag')`
38 |
39 |
40 |
41 | ### `name` (required)
42 | type: `string`
43 |
44 |
45 |
46 | ### `onChange` (required)
47 | type: `func`
48 |
49 |
50 |
51 | ### `selectedIds`
52 | type: `array`
53 |
54 |
55 |
56 | ### `type`
57 | type: `enum('radio'|'checkbox'|'switch')`
58 |
59 |
60 |
61 | ### `value`
62 | type: `arrayOf[object Object]`
63 |
64 |
65 |
--------------------------------------------------------------------------------
/stories/Container.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | export default class Container extends Component {
4 | constructor (props) {
5 | super(props);
6 | this.state = {
7 | value: props.value
8 | };
9 | this.handleChange = this.handleChange.bind(this);
10 | }
11 |
12 | handleChange (data) {
13 | this.setState({
14 | value: data.value
15 | }, () => {
16 | this.props.action('data changed')(data);
17 | });
18 | }
19 |
20 | render () {
21 | const self = this;
22 | const children = React.Children.map(this.props.children, (child) => (
23 | React.cloneElement(child, {
24 | value: self.state.value,
25 | onChange: self.handleChange
26 | }))
27 | );
28 |
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | }
35 | }
36 |
37 | Container.propTypes = {
38 | action: PropTypes.func,
39 | value: PropTypes.oneOfType(
40 | [PropTypes.array, PropTypes.bool]
41 | ),
42 | children: PropTypes.element,
43 | className: PropTypes.string
44 | };
45 |
--------------------------------------------------------------------------------
/stories/Slider.range.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import { Slider } from '../components';
4 | import Container from './Container';
5 |
6 | storiesOf('Slider Component (Range)', module)
7 | .addDecorator((story) => (
8 |
13 | {story()}
14 | ))
15 | .add('Default', () => (
16 |
17 | ))
18 | .add('Read Only', () => (
19 |
20 | ))
21 | .add('Disabled', () => (
22 |
23 | ))
24 | .add('Steps', () => (
25 |
26 | ))
27 | .add('Vertical', () => (
28 |
36 | ));
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Ritesh Kumar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/AutoComplete/Tag.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import autoBind from '../utils/autoBind';
4 |
5 | export default class Tag extends Component {
6 | constructor (props) {
7 | super(props);
8 |
9 | autoBind([
10 | 'handleRemove'
11 | ], this);
12 | }
13 |
14 | handleRemove () {
15 | this.props.onRemove({
16 | id: this.props.id,
17 | text: this.props.text
18 | });
19 | }
20 |
21 | render () {
22 | const { showRemove, id, text } = this.props;
23 | return (
24 |
25 | {text}
26 | {showRemove &&
27 |
31 | ✕
32 |
33 | }
34 |
35 | );
36 | }
37 | }
38 |
39 | Tag.propTypes = {
40 | id: PropTypes.oneOfType([
41 | PropTypes.number, PropTypes.string
42 | ]),
43 | onRemove: PropTypes.func,
44 | showRemove: PropTypes.bool,
45 | text: PropTypes.string.isRequired
46 | };
47 |
--------------------------------------------------------------------------------
/scripts/buildDocs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This example script expects a JSON blob generated by react-docgen as input,
3 | * e.g. react-docgen components/* | buildDocs.sh
4 | */
5 |
6 | var fs = require('fs');
7 | var generateMarkdown = require('./generateMarkdown');
8 | var path = require('path');
9 |
10 | var json = '';
11 | process.stdin.setEncoding('utf8');
12 | process.stdin.on('readable', function() {
13 | var chunk = process.stdin.read();
14 | if (chunk !== null) {
15 | json += chunk;
16 | }
17 | });
18 |
19 | process.stdin.on('end', function() {
20 | buildDocs(JSON.parse(json));
21 | });
22 |
23 | function buildDocs(api) {
24 | // api is an object keyed by filepath. We use the file name as component name.
25 | for (var filepath in api) {
26 | var name = getComponentName(filepath);
27 | var markdown = generateMarkdown(name, api[filepath]);
28 | fs.writeFileSync('mdDocs/' + name + '.md', markdown);
29 | process.stdout.write(filepath + ' -> ' + name + '.md\n');
30 | }
31 | }
32 |
33 | function getComponentName(filepath) {
34 | var name = path.basename(filepath);
35 | var ext;
36 | while ((ext = path.extname(name))) {
37 | name = name.substring(0, name.length - ext.length);
38 | }
39 | return name;
40 | }
41 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Storybook
8 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/stories/Group.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 |
4 | import { Group } from '../components';
5 | import Container from './Container';
6 |
7 | const value = [{
8 | id: 1,
9 | label: 'a',
10 | count: 6
11 | }, {
12 | id: 2,
13 | label: 'b'
14 | }, {
15 | id: 3,
16 | label: 'c'
17 | }];
18 |
19 | const value2 = [{
20 | id: 1,
21 | label: 'a',
22 | count: 6
23 | }, {
24 | id: 2,
25 | label: 'b'
26 | }, {
27 | id: 3,
28 | label: 'c'
29 | }];
30 |
31 | storiesOf('Group Button', module)
32 | .addDecorator((story) => {story()})
33 | .add('Switch', () => (
34 |
39 | ))
40 | .add('Radio', () => (
41 |
46 | ))
47 | .add('Checkbox', () => (
48 |
53 | ))
54 | .add('Radio Tag', () => (
55 |
61 | ))
62 | .add('Checkbox Tag', () => (
63 |
69 | ));
70 |
--------------------------------------------------------------------------------
/components/icons.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'react-filters';
3 | src: url('../assets/fonts/reactfilters.eot');
4 | src: url('../assets/fonts/reactfilters.eot?#iefix') format('embedded-opentype'),
5 | url('../assets/fonts/reactfilters.ttf') format('truetype'),
6 | url('../assets/fonts/reactfilters.woff') format('woff'),
7 | url('../assets/fonts/reactfilters.svg') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | [class^="icon-"], [class*=" icon-"] {
13 | /* use !important to prevent issues with browser extensions that change fonts */
14 | font-family: 'react-filters' !important;
15 | speak: none;
16 | font-style: normal;
17 | font-weight: normal;
18 | font-variant: normal;
19 | text-transform: none;
20 | line-height: 1;
21 |
22 | /* Better Font Rendering =========== */
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | .icon-add:before {
28 | content: "\e145";
29 | }
30 |
31 | .icon-arrow-drop-down:before {
32 | content: "\e5c5";
33 | }
34 |
35 | .icon-arrow-drop-up:before {
36 | content: "\e5c7";
37 | }
38 |
39 | .icon-cancel:before {
40 | content: "\e5c9";
41 | }
42 |
43 | .icon-check-box:before {
44 | content: "\e834";
45 | }
46 |
47 | .icon-check-box-outline-blank:before {
48 | content: "\e835";
49 | }
50 |
51 | .icon-check-circle:before {
52 | content: "\e86c";
53 | }
54 |
55 | .icon-radio-button-unchecked:before {
56 | content: "\e836";
57 | }
58 |
59 | .icon-radio-button-checked:before {
60 | content: "\e837";
61 | }
62 |
63 | .icon-remove:before {
64 | content: "\e15b";
65 | }
66 |
--------------------------------------------------------------------------------
/components/Slider/helpers/getNearestValue.js:
--------------------------------------------------------------------------------
1 | import constants from '../constants';
2 | import { capitalize, isVertical } from '../utils';
3 |
4 | /**
5 | * Returns the nearest value that can be obtained after clicking on a
6 | * particular position on the track. Technically finds the nearest
7 | * slider (upper or lower) and changes the value based on whether the lower or upper
8 | * slider should move to that position.
9 | * @param e [Synthetic Event]
10 | * @param props React Props
11 | * @param trackOffset cached track.getBoundingClientRect()
12 | * @returns {*}
13 | */
14 | export default function (e, props, trackOffset) {
15 | const { value, max, min, step, type, orientation } = props;
16 |
17 | let relativeOffset = e[`page${capitalize(constants[orientation].coordinate)}`]
18 | - trackOffset[constants[orientation].direction];
19 |
20 | if (isVertical(orientation)) relativeOffset = trackOffset.height - relativeOffset;
21 |
22 | const positionOffset = trackOffset[constants[orientation].dimension] / (max - min);
23 | const nearestIntegerValue = Math.round(relativeOffset / positionOffset);
24 | const nearestValue = nearestIntegerValue - (nearestIntegerValue % step);
25 | if (type === 'range') {
26 | const distancesFromValues = [
27 | Math.abs(nearestValue - value[0]),
28 | Math.abs(nearestValue - value[1])
29 | ];
30 | return distancesFromValues[0] < distancesFromValues[1] ? ({
31 | changed: 'lower',
32 | value: [nearestValue, value[1]]
33 | }) : ({
34 | changed: 'upper',
35 | value: [value[0], nearestValue]
36 | });
37 | } else {
38 | return { value: nearestValue };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/test/utils/debounce.test.js:
--------------------------------------------------------------------------------
1 | const { describe, it, before, after, __base } = global;
2 | import { expect } from 'chai';
3 | import sinon from 'sinon';
4 |
5 | const debounce = require(`${__base}components/utils/debounce`).default;
6 |
7 | describe('debounce()', () => {
8 | it('shouldnt execute immediately if immediate is not passed', () => {
9 | const callback = sinon.spy();
10 |
11 | const fn = debounce(callback, 100);
12 |
13 | expect(callback.calledOnce).to.equal(false);
14 | fn();
15 | expect(callback.calledOnce).to.equal(false);
16 | });
17 |
18 | let clock;
19 |
20 | before(() => (clock = sinon.useFakeTimers()));
21 | after(() => (clock.restore()));
22 |
23 | it('should execute the function after delay', () => {
24 | const callback = sinon.spy();
25 |
26 | const fn = debounce(callback, 100);
27 |
28 | fn();
29 |
30 | clock.tick(99);
31 | expect(callback.calledOnce).to.equal(false);
32 |
33 | clock.tick(1);
34 | expect(callback.calledOnce).to.equal(true);
35 | });
36 |
37 | it('should execute at start if immediate flag is passed', () => {
38 | const callback = sinon.spy();
39 | const fn = debounce(callback, 100, true);
40 |
41 | fn();
42 |
43 | expect(callback.calledOnce).to.equal(true);
44 | });
45 |
46 | it('should be called once in interval', () => {
47 | const callback = sinon.spy();
48 | const fn = debounce(callback, 100);
49 |
50 | fn();
51 | fn();
52 | fn();
53 | clock.tick(99);
54 | expect(callback.calledOnce).to.equal(false);
55 |
56 | clock.tick(1);
57 | fn();
58 | fn();
59 | fn();
60 |
61 | expect(callback.calledOnce).to.equal(true);
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/components/InputRange/InputRange.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 | import autoBind from '../utils/autoBind';
4 | import noop from '../utils/noop';
5 |
6 | import AutoComplete from '../AutoComplete/AutoComplete';
7 |
8 | export default class InputRange extends Component {
9 | constructor (props) {
10 | super(props);
11 | this.state = {};
12 |
13 | autoBind([
14 | 'onSelect'
15 | ], this);
16 | }
17 |
18 | onSelect (name, selected) {
19 | this.props.onSelect(name, selected);
20 | }
21 |
22 | render () {
23 | const { name, disabled, suggestions, placeholders } = this.props;
24 | const mainClass = classNames('react-filters', 'rf-input-range', name, { disabled });
25 | return (
26 |
43 | );
44 | }
45 | }
46 |
47 | InputRange.propTypes = {
48 | disabled: PropTypes.bool,
49 | name: PropTypes.string,
50 | onSelect: PropTypes.func.isRequired,
51 | placeholders: PropTypes.arrayOf(PropTypes.string),
52 | suggestions: PropTypes.array
53 | };
54 |
55 | InputRange.defaultProps = {
56 | disabled: false,
57 | onSelect: noop
58 | };
59 |
--------------------------------------------------------------------------------
/mdDocs/Count.md:
--------------------------------------------------------------------------------
1 | `Count` (component)
2 | ===================
3 |
4 |
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [decrementElement](#decrementelement)| |`func`
12 | [disabled](#disabled)| |`bool`
13 | [incrementElement](#incrementelement)| |`func`
14 | [max](#max)| |`number`
15 | [min](#min)| |`number`
16 | [name](#name)|✔️|`string`
17 | [onChange](#onchange)|✔️|`func`
18 | [prefix](#prefix)| |`string`
19 | [step](#step)| |`number`
20 | [suffix](#suffix)| |`string`
21 | [value](#value)| |`number`
22 | ### `decrementElement`
23 | type: `func`
24 |
25 | defaultValue:
26 | ```js
27 | function() {
28 | return (
29 |
32 | );
33 | }
34 | ```
35 |
36 |
37 | ### `disabled`
38 | type: `bool`
39 | defaultValue: `false`
40 |
41 |
42 |
43 | ### `incrementElement`
44 | type: `func`
45 |
46 | defaultValue:
47 | ```js
48 | function() {
49 | return (
50 |
53 | );
54 | }
55 | ```
56 |
57 |
58 | ### `max`
59 | type: `number`
60 | defaultValue: `Number.POSITIVE_INFINITY`
61 |
62 |
63 |
64 | ### `min`
65 | type: `number`
66 | defaultValue: `Number.NEGATIVE_INFINITY`
67 |
68 |
69 |
70 | ### `name` (required)
71 | type: `string`
72 |
73 |
74 |
75 | ### `onChange` (required)
76 | type: `func`
77 |
78 |
79 |
80 | ### `prefix`
81 | type: `string`
82 |
83 |
84 |
85 | ### `step`
86 | type: `number`
87 | defaultValue: `1`
88 |
89 |
90 |
91 | ### `suffix`
92 | type: `string`
93 |
94 |
95 |
96 | ### `value`
97 | type: `number`
98 | defaultValue: `0`
99 |
100 |
101 |
--------------------------------------------------------------------------------
/components/Slider/Rail.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { isVertical } from './utils';
3 | import constants from './constants';
4 |
5 | /**
6 | * Returns rail's position value of `left` for horizontal slider and `top`
7 | * for vertical slider
8 | * @param value
9 | * @param min
10 | * @param max
11 | * @param orientation
12 | * @returns {number} Value in Percentage
13 | */
14 | function getDirectionPositionForRange (value, min, max, orientation) {
15 | return isVertical(orientation) ? (
16 | // as upper value is used to calculate `top`;
17 | Math.round(((max - value[1]) / max - min) * 100)
18 | ) : (
19 | Math.round((value[0] / max - min) * 100)
20 | );
21 | }
22 |
23 | export default function Rail (props) {
24 | const { value, min, max, orientation, isRangeType } = props;
25 |
26 | const difference = isRangeType ? (value[1] - value[0]) : value;
27 | const dimensionValue = (difference / (max - min)) * 100;
28 |
29 | const directionValue = getDirectionPositionForRange(value, min, max, orientation);
30 |
31 | const railStyle = {
32 | [constants[orientation].direction]: `${directionValue}%`,
33 | [constants[orientation].dimension]: `${dimensionValue}%`
34 | };
35 |
36 | if (!isRangeType) {
37 | railStyle[isVertical(orientation) ? 'bottom' : 'left'] = 0;
38 | if (isVertical(orientation)) {
39 | railStyle.top = `${100 - dimensionValue}%`;
40 | } else {
41 | railStyle.left = 0;
42 | }
43 | }
44 |
45 | return ;
46 | }
47 |
48 | Rail.propTypes = {
49 | isRangeType: PropTypes.bool.isRequired,
50 | max: PropTypes.number.isRequired,
51 | min: PropTypes.number.isRequired,
52 | orientation: PropTypes.string.isRequired,
53 | value: PropTypes.array.isRequired
54 | };
55 |
--------------------------------------------------------------------------------
/components/AutoComplete/AutoComplete.scss:
--------------------------------------------------------------------------------
1 | .rf-autocomplete {
2 | position: relative;
3 | }
4 |
5 | .ac-searchbox {
6 | outline: none;
7 | border-radius: 2px;
8 | border: 1px solid #eee;
9 | width: 100%;
10 | box-sizing: border-box;
11 | will-change: border-color;
12 | transition: border-color .2s ease-in;
13 | background-color: #ffffff;
14 | &:focus {
15 | border: 1px solid deepskyblue;
16 | }
17 | .disabled & {
18 | pointer-events: none;
19 | background-color: #fafafa;
20 | }
21 | &.tags{
22 | padding-left: 10px;
23 | }
24 | }
25 |
26 | .ac-searchbox-input{
27 | width: 100%;
28 | padding: 10px;
29 | font-size: 12px;
30 | line-height: 14px;
31 | border:none;
32 | outline: none;
33 | font-family: inherit;
34 | box-sizing: border-box;
35 | background-color: transparent;
36 | .tags &{
37 | display: inline-block;
38 | max-width: 150px;
39 | vertical-align: top;
40 | padding-left: 0;
41 | }
42 | }
43 |
44 | .ac-suggestions-wrapper {
45 | border: 1px solid #eee;
46 | border-top: 0;
47 | background-color: #fff;
48 | position: absolute;
49 | width: 100%;
50 | box-sizing: border-box;
51 | }
52 |
53 | .ac-suggestion {
54 | font-size: 12px;
55 | line-height: 30px;
56 | padding: 0 10px;
57 | color: #666;
58 | border-bottom: 1px solid #eeeeee;
59 | &:last-of-type {
60 | border-bottom: none;
61 | }
62 | &.ac-suggestion-active {
63 | background-color: #fafafa;
64 | }
65 | }
66 |
67 | .ac-reset {
68 | position: absolute;
69 | right: 10px;
70 | top: 8px;
71 | cursor: pointer;
72 | }
73 |
74 | .ac-tag-wrapper{
75 | display: inline-block;
76 | padding: 6px 0;
77 | float: left;
78 | font-size: 12px;
79 | }
80 |
81 | .ac-tag {
82 | display: inline-block;
83 | background-color: cornflowerblue;
84 | color: #fff;
85 | border-radius: 2px;
86 | padding: 0 5px;
87 | line-height: 22px;
88 | margin-right: 5px;
89 | }
90 |
91 | .ac-tag-remove {
92 | padding-left: 5px;
93 | cursor: pointer;
94 | border-left: 1px solid #a5c5ff;
95 | margin-left: 5px;
96 | }
97 |
--------------------------------------------------------------------------------
/assets/fonts/reactfilters.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/0ab516c6e00c001e01c647f19720b2e8.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/components/Slider/Steps.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import classNames from 'classnames';
3 | import constants from './constants';
4 | import { isVertical } from './utils';
5 |
6 | /**
7 | * Tells whether a particular step comes in between two controls or not
8 | * @param stepValue value of the position where this step is located
9 | * @param value Array of control values
10 | * @param isRangeType
11 | * @returns {boolean}
12 | */
13 | function isInActiveRange (stepValue, value, isRangeType) {
14 | if (isRangeType) {
15 | return stepValue > value[0] && stepValue < value[1];
16 | } else {
17 | return stepValue < value;
18 | }
19 | }
20 |
21 | /**
22 | * Returns the step position in percentage
23 | * @param stepValue value of the position where this step is located
24 | * @param min minimum value of slider
25 | * @param max maximum value of slider
26 | * @returns {number}
27 | */
28 | function getPositionInPercentage (stepValue, min, max) {
29 | return (stepValue / (max - min)) * 100;
30 | }
31 |
32 |
33 | /**
34 | * Array of step elements placed side by side
35 | * @param props
36 | * @returns {Array}
37 | */
38 | function getSteps (props) {
39 | const { step, min, max, value, isRangeType, orientation } = props;
40 |
41 | const steps = [];
42 | const totalSteps = ((max - min) / step) + 1;
43 |
44 | for (let i = 0; i < totalSteps; i++) {
45 | let position = getPositionInPercentage(i * step, min, max);
46 | if (isVertical(orientation)) position = 100 - position;
47 |
48 | const style = { [constants[orientation].direction]: `${position}%` };
49 |
50 | const className = classNames('slider-step', {
51 | 'slider-step-active': isInActiveRange(i * step, value, isRangeType)
52 | });
53 | steps.push();
54 | }
55 | return steps;
56 | }
57 |
58 | export default function Steps (props) {
59 | return (
60 |
61 | {getSteps(props)}
62 |
63 | );
64 | }
65 |
66 | Steps.propTypes = {
67 | isRangeType: PropTypes.bool.isRequired,
68 | max: PropTypes.number.isRequired,
69 | min: PropTypes.number.isRequired,
70 | onClick: PropTypes.func.isRequired,
71 | orientation: PropTypes.string.isRequired,
72 | step: PropTypes.number.isRequired
73 | };
74 |
75 |
--------------------------------------------------------------------------------
/mdDocs/Slider.md:
--------------------------------------------------------------------------------
1 | `Slider` (component)
2 | ====================
3 |
4 |
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [attributes](#attributes)| |`object`
12 | [disabled](#disabled)| |`bool`
13 | [max](#max)| |`number`
14 | [min](#min)| |`number`
15 | [name](#name)|✔️|`string`
16 | [onChange](#onchange)| |`func`
17 | [onDragEnd](#ondragend)| |`func`
18 | [onDragStart](#ondragstart)| |`func`
19 | [orientation](#orientation)| |`enum('horizontal'|'vertical')`
20 | [rangeTemplate](#rangetemplate)| |`func`
21 | [readOnly](#readonly)| |`bool`
22 | [showSteps](#showsteps)| |`bool`
23 | [step](#step)| |`number`
24 | [toolTipTemplate](#tooltiptemplate)| |`func`
25 | [type](#type)| |`enum('value'|'range')`
26 | [value](#value)| |`union(array|number)`
27 | ### `attributes`
28 | type: `object`
29 | defaultValue: `{}`
30 |
31 |
32 |
33 | ### `disabled`
34 | type: `bool`
35 | defaultValue: `false`
36 |
37 |
38 |
39 | ### `max`
40 | type: `number`
41 | defaultValue: `20`
42 |
43 |
44 |
45 | ### `min`
46 | type: `number`
47 | defaultValue: `0`
48 |
49 |
50 |
51 | ### `name` (required)
52 | type: `string`
53 |
54 |
55 |
56 | ### `onChange`
57 | type: `func`
58 |
59 |
60 |
61 | ### `onDragEnd`
62 | type: `func`
63 | defaultValue: `noop`
64 |
65 |
66 |
67 | ### `onDragStart`
68 | type: `func`
69 | defaultValue: `noop`
70 |
71 |
72 |
73 | ### `orientation`
74 | type: `enum('horizontal'|'vertical')`
75 | defaultValue: `'horizontal'`
76 |
77 |
78 |
79 | ### `rangeTemplate`
80 | type: `func`
81 |
82 | defaultValue:
83 | ```js
84 | function(min, max) {
85 | return (
86 |
87 |
{min}
88 |
{max}
89 |
90 | );
91 | }
92 | ```
93 |
94 |
95 | ### `readOnly`
96 | type: `bool`
97 | defaultValue: `false`
98 |
99 |
100 |
101 | ### `showSteps`
102 | type: `bool`
103 | defaultValue: `false`
104 |
105 |
106 |
107 | ### `step`
108 | type: `number`
109 | defaultValue: `1`
110 |
111 |
112 |
113 | ### `toolTipTemplate`
114 | type: `func`
115 |
116 | defaultValue:
117 | ```js
118 | function(value) {
119 | return value;
120 | }
121 | ```
122 |
123 |
124 | ### `type`
125 | type: `enum('value'|'range')`
126 | defaultValue: `'value'`
127 |
128 |
129 |
130 | ### `value`
131 | type: `union(array|number)`
132 | defaultValue: `[5, 10]`
133 |
134 |
135 |
--------------------------------------------------------------------------------
/scripts/generateMarkdown.js:
--------------------------------------------------------------------------------
1 | function stringOfLength(string, length) {
2 | var newString = '';
3 | for (var i = 0; i < length; i++) {
4 | newString += string;
5 | }
6 | return newString;
7 | }
8 |
9 | function generateTitle(name) {
10 | var title = '`' + name + '` (component)';
11 | return title + '\n' + stringOfLength('=', title.length) + '\n';
12 | }
13 |
14 | function generateDesciption(description) {
15 | return description + '\n';
16 | }
17 |
18 | function generatePropType(type, includeType = true) {
19 | var values;
20 | if (Array.isArray(type.value)) {
21 | values = '(' +
22 | type.value.map(function(typeValue) {
23 | return typeValue.name || typeValue.value;
24 | }).join('|') +
25 | ')';
26 | } else {
27 | values = type.value;
28 | }
29 |
30 | return (includeType ? 'type: `' : '`') + type.name + (values ? values: '') + '`\n';
31 | }
32 |
33 | function generatePropDefaultValue(value) {
34 | return value.value.indexOf('function') >= 0 ?
35 | '\ndefaultValue: \n```js\n' + value.value + '\n```' : 'defaultValue: `' + value.value + '`\n';
36 | }
37 |
38 | function generateProp(propName, prop) {
39 | return (
40 | '### `' + propName + '`' + (prop.required ? ' (required)' : '') + '\n' +
41 | (prop.type ? generatePropType(prop.type) : '') +
42 | (prop.defaultValue ? generatePropDefaultValue(prop.defaultValue) : '') +
43 | '\n' +
44 | (prop.description ? prop.description + '\n\n' : '') +
45 | '\n'
46 | );
47 | }
48 |
49 | function propTable (propName, prop) {
50 | return (
51 | '[' +propName + '](#'+ propName.toLowerCase() +')' + '|' + (prop.required ? '✔️' : ' ') + '|' + (prop.type ? generatePropType(prop.type, false) : '\n')
52 | )
53 | }
54 |
55 | function generateProps(props) {
56 | var title = 'Props';
57 |
58 | return (
59 | title + '\n' +
60 | stringOfLength('-', title.length) + '\n' +
61 | '\n' +
62 | 'prop name | isRequired | type\n-------|------|------\n' +
63 | Object.keys(props).sort().map(function (propName) {
64 | return propTable(propName, props[propName])
65 | }).join('')+
66 | Object.keys(props).sort().map(function(propName) {
67 | return generateProp(propName, props[propName]);
68 | }).join('\n')
69 | );
70 | }
71 |
72 | function generateMarkdown(name, reactAPI) {
73 | return generateTitle(name) + '\n' +
74 | generateDesciption(reactAPI.description) + '\n' +
75 | generateProps(reactAPI.props);
76 | }
77 |
78 | module.exports = generateMarkdown;
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | > A collection of Components like autocomplete, radio, checkbox, slider etc. written in React.
20 |
21 | ##Installation
22 | ```
23 | npm install --save react-filters
24 | ```
25 |
26 | ##Basic Usage
27 | import the component you need to use.
28 |
29 | ```js
30 | import { Slider, Toggle } from 'react-filters/dist';
31 | ```
32 |
33 | If you don't want to use all the components and are concerned about file size, you can just import the component you need.
34 |
35 | ```js
36 | import Slider from 'react-filters/dist/Slider';
37 | import Toggle from 'react-filters/dist/Toggle';
38 | ```
39 |
40 | ##Components
41 |
42 | Click on the component name to go to their documentation.
43 |
44 | - [x] [Toggle](mdDocs/Toggle.md) (Switch, Checkbox, Radio)
45 | - [x] [Slider](mdDocs/Slider.md)
46 | - [x] [Autocomplete](mdDocs/AutoComplete.md)
47 | - [x] [Input Range](mdDocs/InputRange.md)
48 | - [x] [Count](mdDocs/Count.md)
49 | - [ ] Select
50 | - [ ] Dropdown
51 |
52 | ##Development
53 | 1. Clone the repo
54 | 1. Create a new branch.
55 | 1. Run `npm install && npm run storybook`
56 | 1. You can find the server running at **localhost:9002**
57 | 1. Add feature or fix bug. Add tests if required.
58 | 1. if commit fails make sure that there's no linting error or failed test by running `npm run test && npm run lint`
59 |
60 |
61 | ##License
62 | MIT @ Ritesh Kumar
63 |
--------------------------------------------------------------------------------
/mdDocs/AutoComplete.md:
--------------------------------------------------------------------------------
1 | `AutoComplete` (component)
2 | ==========================
3 |
4 |
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [Reset](#reset)| |`func`
12 | [async](#async)| |`bool`
13 | [className](#classname)| |`string`
14 | [debounce](#debounce)| |`number`
15 | [disabled](#disabled)| |`bool`
16 | [fuzzyOptions](#fuzzyoptions)| |`shape[object Object]`
17 | [list](#list)| |`array`
18 | [multiSelect](#multiselect)| |`bool`
19 | [name](#name)|✔️|`string`
20 | [onBlur](#onblur)| |`func`
21 | [onChange](#onchange)| |`func`
22 | [onFocus](#onfocus)| |`func`
23 | [onSelect](#onselect)|✔️|`func`
24 | [placeholder](#placeholder)| |`string`
25 | [resultsTemplate](#resultstemplate)| |`func`
26 | [showInitialResults](#showinitialresults)| |`bool`
27 | [showTagRemove](#showtagremove)| |`bool`
28 | [tags](#tags)| |
29 | [valueKey](#valuekey)| |`string`
30 | [width](#width)| |`number`
31 | ### `Reset`
32 | type: `func`
33 |
34 |
35 |
36 | ### `async`
37 | type: `bool`
38 | defaultValue: `false`
39 |
40 |
41 |
42 | ### `className`
43 | type: `string`
44 |
45 |
46 |
47 | ### `debounce`
48 | type: `number`
49 | defaultValue: `250`
50 |
51 |
52 |
53 | ### `disabled`
54 | type: `bool`
55 | defaultValue: `false`
56 |
57 |
58 |
59 | ### `fuzzyOptions`
60 | type: `shape[object Object]`
61 | defaultValue: `{
62 | caseSensitive: false,
63 | shouldSort: true,
64 | sortFn (a, b) {
65 | return a.score - b.score;
66 | },
67 | threshold: 0.6,
68 | tokenize: false,
69 | verbose: false,
70 | distance: 100,
71 | include: [],
72 | location: 0
73 | }`
74 |
75 |
76 |
77 | ### `list`
78 | type: `array`
79 |
80 |
81 |
82 | ### `multiSelect`
83 | type: `bool`
84 | defaultValue: `false`
85 |
86 |
87 |
88 | ### `name` (required)
89 | type: `string`
90 |
91 |
92 |
93 | ### `onBlur`
94 | type: `func`
95 |
96 |
97 |
98 | ### `onChange`
99 | type: `func`
100 |
101 |
102 |
103 | ### `onFocus`
104 | type: `func`
105 |
106 |
107 |
108 | ### `onSelect` (required)
109 | type: `func`
110 |
111 |
112 |
113 | ### `placeholder`
114 | type: `string`
115 | defaultValue: `'Search'`
116 |
117 |
118 |
119 | ### `resultsTemplate`
120 | type: `func`
121 | defaultValue: `Suggestions.defaultResultsTemplate`
122 |
123 |
124 |
125 | ### `showInitialResults`
126 | type: `bool`
127 | defaultValue: `false`
128 |
129 |
130 |
131 | ### `showTagRemove`
132 | type: `bool`
133 | defaultValue: `true`
134 |
135 |
136 |
137 | ### `tags`
138 | defaultValue: `false`
139 |
140 |
141 |
142 | ### `valueKey`
143 | type: `string`
144 | defaultValue: `'title'`
145 |
146 |
147 |
148 | ### `width`
149 | type: `number`
150 | defaultValue: `430`
151 |
152 |
153 |
--------------------------------------------------------------------------------
/test/Count.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const { describe, it, __base } = global;
3 | import { shallow } from 'enzyme';
4 | import sinon from 'sinon';
5 | import { expect } from 'chai';
6 |
7 | const { Count } = require(`${__base}components`);
8 |
9 | describe('Count Component', () => {
10 | it('should call onChange function with correct args on click', () => {
11 | const onChange = sinon.spy();
12 | const wrapper = shallow(
13 |
18 | );
19 |
20 | const args1 = {
21 | name: 'count',
22 | value: 6,
23 | action: 'increased'
24 | };
25 |
26 | const args2 = {
27 | name: 'count',
28 | value: 4,
29 | action: 'decreased'
30 | };
31 |
32 | wrapper.find('.cb-upper').simulate('click');
33 | expect(onChange.calledWith(args1)).to.equal(true);
34 | wrapper.find('.cb-lower').simulate('click');
35 | expect(onChange.calledWith(args2)).to.equal(true);
36 |
37 | expect(onChange.calledTwice).to.equal(true);
38 | });
39 |
40 | it('should be inactive when disabled', () => {
41 | const onChange = sinon.spy();
42 | const wrapper = shallow(
43 |
49 | );
50 |
51 | wrapper.find('.cb-lower').simulate('click');
52 | expect(onChange.calledOnce).to.equal(false);
53 | });
54 |
55 | it('should only be active in the provided range', () => {
56 | const onChange = sinon.spy();
57 | const wrapper = shallow(
58 |
65 | );
66 |
67 | wrapper.find('.cb-upper').simulate('click');
68 | expect(onChange.calledOnce).to.equal(false);
69 |
70 | wrapper.find('.cb-lower').simulate('click');
71 | expect(onChange.calledOnce).to.equal(true);
72 | });
73 |
74 | it('should change with the provided step', () => {
75 | const onChange = sinon.spy();
76 | const wrapper = shallow(
77 |
83 | );
84 |
85 | wrapper.find('.cb-upper').simulate('click');
86 | const args1 = {
87 | name: 'count',
88 | value: 7,
89 | action: 'increased'
90 | };
91 |
92 | expect(onChange.calledWith(args1)).to.equal(true);
93 |
94 | wrapper.find('.cb-lower').simulate('click');
95 | const args2 = {
96 | name: 'count',
97 | value: 3,
98 | action: 'decreased'
99 | };
100 |
101 | expect(onChange.calledWith(args2)).to.equal(true);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/components/Toggle/Toggle.scss:
--------------------------------------------------------------------------------
1 | @import "../theme";
2 | @import "../mixins";
3 |
4 | $toggleContainerWidth: 200px !default;
5 |
6 | .rf-toggle {
7 | display: inline-block;
8 | i{
9 | color: $tertiary-color;
10 | transition: all .3s ease-out;
11 | }
12 | &.toggle-active i{
13 | color: $primary-color;
14 | }
15 | &.toggle-tag.toggle-disabled{
16 | background-color: $secondary-color;
17 | border: darken($secondary-color, 35%);
18 | }
19 | }
20 |
21 | .toggle-wrapper {
22 | background-color: darken($secondary-color, 30%);
23 | width: $toggle-width;
24 | height: $toggle-height;
25 | border-radius: $toggle-height/2;
26 | padding: $toggle-padding;
27 | display: inline-block;
28 | position: relative;
29 | transition: all .3s ease-out;
30 | box-sizing: border-box;
31 | .toggle-active & {
32 | background-color: $primary-color;
33 | }
34 | .toggle-disabled & {
35 | background-color: grey;
36 | opacity: 0.5;
37 | pointer-events: none;
38 | }
39 | }
40 |
41 | .toggle-tag{
42 | border: 1px solid $primary-color;
43 | border-radius: $toggle-height;
44 | cursor: pointer;
45 | &:hover{
46 | background-color: $secondary-color;
47 | }
48 | &.toggle-active{
49 | background-color: $primary-color;
50 | &:hover{
51 | background-color: darken($primary-color, 5%);
52 | }
53 | .toggle-count, .toggle-label{
54 | color: #ffffff;
55 | }
56 | }
57 | }
58 |
59 | .toggle-btn {
60 | height: $toggle-height - ($toggle-padding * 2);
61 | width: $toggle-height - ($toggle-padding * 2);
62 | position: absolute;
63 | background-color: $toggle-btn-color;
64 | border-radius: 50%;
65 | cursor: pointer;
66 | left: $toggle-padding;
67 | display: inline-block;
68 | transition: all .3s ease-out;
69 | transform: translate3d(0, 0, 0);
70 | .toggle-active & {
71 | left: 100%;
72 | margin-left: -($toggle-height - $toggle-padding);
73 | }
74 | }
75 |
76 | .toggle-label {
77 | display: inline-block;
78 | vertical-align: top;
79 | width: $toggleContainerWidth - $toggle-width;
80 | @include no-select();
81 | .after {
82 | float: right;
83 | }
84 | .toggle-tag &{
85 | text-align: center;
86 | padding: 6px 0;
87 | line-height: 20px;
88 | }
89 | }
90 |
91 | .toggle-count {
92 | color: $toggle-count-color;
93 | margin-left: 5px;
94 | }
95 |
96 | .toggle-icon-label {
97 | font-size: $toggle-label-font-size;
98 | line-height: $toggle-label-line-height;
99 | width: $toggle-width - ($toggle-height + 2 * $toggle-padding);
100 | text-align: center;
101 | color: white;
102 | &.toggle-il-right {
103 | float: right;
104 | padding-right: $toggle-padding;
105 | }
106 | &.toggle-il-left {
107 | float: left;
108 | padding-left: $toggle-padding;
109 | }
110 | }
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/mdDocs/Toggle.md:
--------------------------------------------------------------------------------
1 | `Toggle` (component)
2 | ====================
3 |
4 | Hello world
5 |
6 | Props
7 | -----
8 |
9 | prop name | isRequired | type
10 | -------|------|------
11 | [attributes](#attributes)| |`object`
12 | [className](#classname)| |`string`
13 | [count](#count)| |`number`
14 | [countElem](#countelem)| |`union(func|element)`
15 | [disabled](#disabled)| |`bool`
16 | [iconElement](#iconelement)| |`func`
17 | [iconLabel](#iconlabel)| |`array`
18 | [label](#label)| |`string`
19 | [labelPosition](#labelposition)| |`enum('before'|'after')`
20 | [mode](#mode)| |`enum('normal'|'tag')`
21 | [name](#name)|✔️|`string`
22 | [onChange](#onchange)| |`func`
23 | [type](#type)| |`enum('switch'|'radio'|'checkbox')`
24 | [value](#value)|✔️|`bool`
25 | ### `attributes`
26 | type: `object`
27 |
28 | Sometimes you may need to add some custom attributes to the root tag of the
29 | component. attributes will accept an object where the key and values will
30 | be those attributes and their value respectively.
31 |
32 | Eg : If you pass
33 | ```js
34 | attributes = {
35 | 'data-attr1' : 'val1',
36 | 'data-attr2' : 'val2'
37 | }
38 | ```
39 | the root tag will have the attributes `data-attr1` and `data-attr2` with the
40 | corresponding values as `val1` and `val2` respectively
41 |
42 |
43 |
44 | ### `className`
45 | type: `string`
46 |
47 | Optional className to be added to the root tag of the component
48 |
49 |
50 |
51 | ### `count`
52 | type: `number`
53 |
54 | In case you want to show aggregation/count in front of label then pass the
55 | number in this option. This is generally useful for showing the items present
56 | corresponding to that filter option.
57 |
58 |
59 |
60 | ### `countElem`
61 | type: `union(func|element)`
62 |
63 | defaultValue:
64 | ```js
65 | function(p) {
66 | return ({p.count});
67 | }
68 | ```
69 |
70 |
71 | ### `disabled`
72 | type: `bool`
73 | defaultValue: `false`
74 |
75 | Set to `true` if you want to disable the component interactions.
76 |
77 |
78 |
79 | ### `iconElement`
80 | type: `func`
81 |
82 |
83 |
84 | ### `iconLabel`
85 | type: `array`
86 |
87 |
88 |
89 | ### `label`
90 | type: `string`
91 |
92 | The label text present in the component. If this option is not set only the
93 | icon element will render.
94 |
95 |
96 |
97 | ### `labelPosition`
98 | type: `enum('before'|'after')`
99 | defaultValue: `'before'`
100 |
101 |
102 |
103 | ### `mode`
104 | type: `enum('normal'|'tag')`
105 | defaultValue: `'normal'`
106 |
107 |
108 |
109 | ### `name` (required)
110 | type: `string`
111 |
112 |
113 |
114 | ### `onChange`
115 | type: `func`
116 | defaultValue: `noop`
117 |
118 |
119 |
120 | ### `type`
121 | type: `enum('switch'|'radio'|'checkbox')`
122 | defaultValue: `'switch'`
123 |
124 |
125 |
126 | ### `value` (required)
127 | type: `bool`
128 | defaultValue: `false`
129 |
130 |
131 |
--------------------------------------------------------------------------------
/stories/AutoComplete.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import classNames from 'classnames';
4 | import { AutoComplete } from '../components';
5 |
6 |
7 | const list = [{
8 | id: 1,
9 | title: 'The Great Gatsby',
10 | author: 'F. Scott Fitzgerald'
11 | }, {
12 | id: 2,
13 | title: 'The DaVinci Code',
14 | author: 'Dan Brown'
15 | }, {
16 | id: 3,
17 | title: 'Angels & Demons',
18 | author: 'Dan Brown'
19 | }];
20 |
21 | class AutoCompleteContainer extends React.Component {
22 | constructor (props) {
23 | super(props);
24 | this.state = {
25 | data: []
26 | };
27 |
28 | this.onChange = this.onChange.bind(this);
29 | this.onSelect = this.onSelect.bind(this);
30 | }
31 |
32 | onChange (query) {
33 | const url = `https://buy.housing.com/api/v0/search/suggest/?&source=web&polygon_uuid=a0fd32816f73961748cf&cursor=1&string=${query}`;
34 | if (query.length) {
35 | fetch(url)
36 | .then((x) => x.json())
37 | .then((data) => {
38 | this.setState({ data });
39 | });
40 | }
41 | }
42 |
43 | onSelect (name, selected) {
44 | this.setState({ data: [] });
45 | action('selected')(name, selected);
46 | }
47 |
48 | resultTemplate (val, i, selectedIndex) {
49 | const className = classNames('ac-suggestion', {
50 | 'ac-suggestion-active': i === selectedIndex
51 | });
52 | return {val.name} {val.type}
;
53 | }
54 |
55 | render () {
56 | return (
57 |
68 | );
69 | }
70 | }
71 |
72 | AutoCompleteContainer.propTypes = {
73 | multiSelect: React.PropTypes.bool
74 | };
75 |
76 | storiesOf('AutoComplete', module)
77 | .add('Basic', () => (
78 |
84 | ))
85 | .add('Disabled', () => (
86 |
93 | ))
94 | .add('Async requests', () => )
95 | .add('Fuzzy Search', () => (
96 |
102 | ))
103 | .add('Initial Suggestions', () => (
104 |
111 | ))
112 | .add('Tags', () => (
113 |
114 | ));
115 |
--------------------------------------------------------------------------------
/components/Count/Count.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import autoBind from '../utils/autoBind';
5 |
6 | function inRange (value, min, max) {
7 | return value >= min && value <= max;
8 | }
9 |
10 | export default class Count extends Component {
11 | constructor (props) {
12 | super(props);
13 |
14 | autoBind([
15 | 'handleIncrement',
16 | 'handleDecrement',
17 | 'onChange'
18 | ], this);
19 | }
20 |
21 | shouldComponentUpdate (newProps) {
22 | const { value, min, max } = newProps;
23 | return (
24 | inRange(value, min, max) &&
25 | (value !== this.props.value)
26 | );
27 | }
28 |
29 | onChange (value, action) {
30 | const { name, onChange, min, max } = this.props;
31 |
32 | if (inRange(value, min, max)) {
33 | onChange({
34 | action,
35 | name,
36 | value
37 | });
38 | }
39 | }
40 |
41 | handleDecrement () {
42 | this.onChange(this.props.value - this.props.step, 'decreased');
43 | }
44 |
45 | handleIncrement () {
46 | this.onChange(this.props.value + this.props.step, 'increased');
47 | }
48 |
49 | render () {
50 | const {
51 | name,
52 | disabled,
53 | value,
54 | prefix,
55 | suffix,
56 | decrementElement,
57 | incrementElement
58 | } = this.props;
59 |
60 | const mainClass = classNames('react-filters', 'rf-count', name, { disabled });
61 | return (
62 |
63 |
67 | {decrementElement(this.props)}
68 |
69 |
70 |
71 | {prefix}
72 | {value}
73 | {suffix}
74 |
75 |
79 | {incrementElement(this.props)}
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | Count.propTypes = {
87 | decrementElement: PropTypes.func,
88 | disabled: PropTypes.bool,
89 | incrementElement: PropTypes.func,
90 | max: PropTypes.number,
91 | min: PropTypes.number,
92 | name: PropTypes.string.isRequired,
93 | onChange: PropTypes.func.isRequired,
94 | prefix: PropTypes.string,
95 | step: PropTypes.number,
96 | suffix: PropTypes.string,
97 | value: PropTypes.number
98 | };
99 |
100 | Count.defaultProps = {
101 | decrementElement () {
102 | return (
103 |
106 | );
107 | },
108 | disabled: false,
109 | incrementElement () {
110 | return (
111 |
114 | );
115 | },
116 | max: Number.POSITIVE_INFINITY,
117 | min: Number.NEGATIVE_INFINITY,
118 | step: 1,
119 | value: 0
120 | };
121 |
--------------------------------------------------------------------------------
/components/AutoComplete/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 | import Tag from './Tag';
4 |
5 | import autoBind from '../utils/autoBind';
6 |
7 | export default class SearchBox extends Component {
8 | constructor (props) {
9 | super(props);
10 |
11 | autoBind([
12 | 'handleQueryChange',
13 | 'removeLastTag'
14 | ], this);
15 | }
16 |
17 | getTags () {
18 | return this.props.multiSelected.map((val, i) => (
19 |
25 | ));
26 | }
27 |
28 | getTagContainer () {
29 | const { multiSelect, multiSelected } = this.props;
30 | if (multiSelect && multiSelected && multiSelected.length) {
31 | return (
32 |
33 | {this.getTags()}
34 |
35 | );
36 | }
37 | return null;
38 | }
39 |
40 | handleQueryChange () {
41 | this.props.onQueryChange(this.refs.autocomplete.value);
42 | }
43 |
44 | removeLastTag (e) {
45 | const { onTagRemove, multiSelected } = this.props;
46 | const keyCode = e.which || e.keyCode;
47 | if (keyCode === 8 && multiSelected && multiSelected.length && !this.props.value.length) {
48 | e.preventDefault();
49 | e.stopPropagation();
50 | onTagRemove({
51 | id: multiSelected.length - 1
52 | });
53 | }
54 | }
55 |
56 | render () {
57 | const {
58 | onFocus,
59 | onBlur,
60 | disabled,
61 | placeholder,
62 | value,
63 | Reset,
64 | onReset,
65 | multiSelect
66 | } = this.props;
67 |
68 | const mainClass = classNames('ac-searchbox', {
69 | tags: multiSelect
70 | });
71 |
72 | return (
73 |
74 | {this.getTagContainer()}
75 |
76 |
88 |
89 |
93 | {value.length > 0 && }
94 |
95 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
102 | function noop () {
103 |
104 | }
105 |
106 | SearchBox.propTypes = {
107 | Reset: PropTypes.element.isRequired,
108 | disabled: PropTypes.bool,
109 | multiSelect: PropTypes.bool,
110 | multiSelected: PropTypes.array,
111 | onBlur: PropTypes.func,
112 | onFocus: PropTypes.func,
113 | onQueryChange: PropTypes.func.isRequired,
114 | onReset: PropTypes.func,
115 | onTagRemove: PropTypes.func,
116 | placeholder: PropTypes.string,
117 | showTagRemove: PropTypes.bool,
118 | value: PropTypes.string.isRequired,
119 | valueKey: PropTypes.string
120 | };
121 |
122 | function ResetContent () {
123 | return ;
124 | }
125 |
126 | SearchBox.defaultProps = {
127 | Reset: ResetContent,
128 | onBlur: noop,
129 | onFocus: noop,
130 | placeholder: 'Search'
131 | };
132 |
--------------------------------------------------------------------------------
/components/Group/Group.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 | import autoBind from '../utils/autoBind';
4 | import lastElement from '../utils/lastElement';
5 | import getSelectedIds from '../utils/getSelectedIds';
6 |
7 | import deepCopy from 'deep-copy';
8 |
9 | import Toggle from '../Toggle/Toggle';
10 |
11 | function handleSingleSelect (arr, index) {
12 | return arr.map((val, i) => {
13 | const obj = val;
14 | obj.value = (i === index) ? !val.value : false;
15 | return obj;
16 | });
17 | }
18 |
19 | function formatValue (props) {
20 | const { value, selectedIds, id } = props;
21 | if (!selectedIds) return value;
22 | const value$ = deepCopy(value);
23 | return value$.map(val => {
24 | if ((typeof selectedIds === 'object' && selectedIds.indexOf(val[id]) >= 0) ||
25 | (typeof selectedIds === 'number' && selectedIds === val[id])) val.value = true;
26 | return val;
27 | });
28 | }
29 |
30 | export default class Group extends Component {
31 | constructor (props) {
32 | super(props);
33 |
34 | this.formattedValue = formatValue(props);
35 |
36 | autoBind([
37 | 'handleChange'
38 | ], this);
39 | }
40 |
41 | componentDidMount () {
42 | this.formattedValue = formatValue(this.props);
43 | }
44 |
45 | componentWillReceiveProps (newProps) {
46 | this.formattedValue = formatValue(newProps);
47 | }
48 |
49 | getElements () {
50 | const { type, mode, disabled } = this.props;
51 | return this.formattedValue.map((val, i) => (
52 |
62 | ));
63 | }
64 |
65 | handleChange (data) {
66 | const { name, type, id, value } = this.props;
67 | let newValue = deepCopy(this.formattedValue);
68 |
69 | const index = parseInt(lastElement(data.name.split('-')), 10);
70 |
71 | if (type === 'checkbox' || type === 'switch') {
72 | newValue[index].value = data.value;
73 | } else {
74 | newValue = handleSingleSelect(newValue, index);
75 | }
76 |
77 | this.props.onChange({
78 | index,
79 | name,
80 | oldValue: value,
81 | value: newValue,
82 | selectedIds: getSelectedIds(newValue, id, type === 'radio')
83 | });
84 | }
85 |
86 | render () {
87 | const { name, className, attributes, type } = this.props;
88 | const mainClass = classNames('rf-group', `${type}-group`, name, className);
89 | return (
90 |
91 | {this.getElements()}
92 |
93 | );
94 | }
95 | }
96 |
97 | Group.propTypes = {
98 | attributes: PropTypes.object,
99 | className: PropTypes.string,
100 | mode: PropTypes.oneOf(['normal', 'tag']),
101 | name: PropTypes.string.isRequired,
102 | onChange: PropTypes.func.isRequired,
103 | type: PropTypes.oneOf([
104 | 'radio', 'checkbox', 'switch'
105 | ]),
106 | value: PropTypes.arrayOf(
107 | PropTypes.shape({
108 | count: PropTypes.number,
109 | label: PropTypes.string,
110 | value: PropTypes.bool
111 | })
112 | ),
113 | selectedIds: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
114 | id: PropTypes.string,
115 | disabled: PropTypes.bool
116 | };
117 |
118 | Group.defaultProps = {
119 | id: 'id',
120 | disabled: false
121 | };
122 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-filters",
3 | "description": "A collection of filters made in React",
4 | "main": "dist/index.js",
5 | "jsnext:main": "components/index.js",
6 | "files": [
7 | "components",
8 | "dist",
9 | "README",
10 | "docs"
11 | ],
12 | "scripts": {
13 | "lint": "eslint --format=node_modules/eslint-formatter-pretty components/**/*.js test/**/*.js stories/*.js",
14 | "lintfix": "eslint --fix components/**/*.js test/**/*.js stories/*.js",
15 | "prepublish": "npm run test && babel components --out-dir dist",
16 | "prebuild": "rm -rf dist/*",
17 | "storybook": "start-storybook -p 9002",
18 | "semantic-release": "semantic-release pre && npm publish && semantic-release post",
19 | "test": "mocha --require test/config/setup 'test/**/*.test.js'",
20 | "test:cover": "istanbul cover -x *.test.js _mocha -- -R spec --require test/config/setup 'test/**/*.test.js'",
21 | "test:report": "cat ./coverage/lcov.info | codecov && rm -rf ./coverage",
22 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
23 | "build": "build-storybook -o docs",
24 | "docs": "react-docgen ./components/**/*.jsx | node scripts/buildDocs.js"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/ritz078/react-filters.git"
29 | },
30 | "keywords": [
31 | "react-filters",
32 | "filters",
33 | "switch",
34 | "dropdown"
35 | ],
36 | "author": "Ritesh Kumar",
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/ritz078/react-filters/issues"
40 | },
41 | "homepage": "https://github.com/ritz078/react-filters",
42 | "devDependencies": {
43 | "@kadira/react-storybook-decorator-centered": "^1.0.0",
44 | "@kadira/storybook": "^2.3.0",
45 | "@kadira/storybook-deployer": "^1.0.0",
46 | "autoprefixer": "^6.3.6",
47 | "babel": "^6.5.2",
48 | "babel-cli": "^6.11.4",
49 | "babel-core": "^6.9.0",
50 | "babel-eslint": "^6.0.2",
51 | "babel-loader": "^6.2.4",
52 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
53 | "babel-plugin-transform-react-remove-prop-types": "^0.2.9",
54 | "babel-preset-es2015": "^6.9.0",
55 | "babel-preset-react": "^6.5.0",
56 | "babel-preset-stage-0": "^6.5.0",
57 | "babel-register": "^6.9.0",
58 | "chai": "^3.5.0",
59 | "codecov.io": "^0.1.6",
60 | "commitizen": "^2.8.1",
61 | "conventional-changelog-cli": "^1.2.0",
62 | "css-loader": "^0.23.1",
63 | "cz-conventional-changelog": "^1.1.6",
64 | "enzyme": "^2.3.0",
65 | "eslint": "^2.13.1",
66 | "eslint-config-airbnb": "^9.0.1",
67 | "eslint-formatter-pretty": "^0.2.2",
68 | "eslint-plugin-import": "^1.8.1",
69 | "eslint-plugin-jsx-a11y": "^1.5.5",
70 | "eslint-plugin-react": "^5.1.1",
71 | "eventsource-polyfill": "*",
72 | "file-loader": "^0.9.0",
73 | "ghooks": "^1.2.4",
74 | "isparta": "^4.0.0",
75 | "istanbul": "^1.1.0-alpha.1",
76 | "jsdom": "^9.0.0",
77 | "mocha": "^3.0.1",
78 | "node-sass": "^3.7.0",
79 | "raw-loader": "^0.5.1",
80 | "react-addons-test-utils": "*",
81 | "react-docgen": "^2.10.0",
82 | "redux": "^3.5.2",
83 | "rimraf": "^2.5.2",
84 | "sass-loader": "^3.2.0",
85 | "semantic-release": "^4.3.5",
86 | "sinon": "^1.17.4",
87 | "style-loader": "^0.13.1",
88 | "url-loader": "^0.5.7",
89 | "webpack-hot-middleware": "^2.10.0"
90 | },
91 | "dependencies": {
92 | "classnames": "^2.2.5",
93 | "deep-copy": "^1.1.2",
94 | "fuse.js": "^2.2.0",
95 | "invariant": "^2.2.1",
96 | "react": "0.14.x || ^15.x.x",
97 | "react-dom": "0.14.x || ^15.x.x"
98 | },
99 | "config": {
100 | "commitizen": {
101 | "path": "node_modules/cz-conventional-changelog"
102 | },
103 | "ghooks": {
104 | "pre-commit": "npm run test"
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/test/Group.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const { describe, it, before, __base } = global;
3 | import { shallow, mount } from 'enzyme';
4 | import sinon from 'sinon';
5 | import { expect } from 'chai';
6 |
7 | const { Group } = require(`${__base}components`);
8 |
9 | describe('Group Component', () => {
10 | let value;
11 |
12 | before(() => (
13 | value = [{
14 | id:1,
15 | label: 'a',
16 | count: 6
17 | }, {
18 | id:2,
19 | label: 'b'
20 | }, {
21 | id:3,
22 | label: 'c'
23 | }]
24 | ));
25 |
26 | it('should render Toggle component', () => {
27 | const onChange = sinon.spy();
28 | const wrapper = shallow(
29 |
35 | );
36 |
37 | expect(wrapper.hasClass('switch-group')).to.equal(true);
38 | expect(wrapper.find('Toggle')).to.have.length(3);
39 | });
40 |
41 | it('should call onChange on click with correct arguments', () => {
42 | const onChange = sinon.spy();
43 | const wrapper = mount(
44 |
50 | );
51 |
52 | wrapper.find('Toggle').at(1).simulate('click');
53 |
54 | const args = {
55 | name: 'change',
56 | value: [{
57 | id:1,
58 | label: 'a',
59 | count: 6
60 | }, {
61 | id:2,
62 | label: 'b',
63 | value: true
64 | }, {
65 | id:3,
66 | label: 'c'
67 | }],
68 | oldValue: value,
69 | index: 1,
70 | selectedIds: [2]
71 | };
72 |
73 | expect(onChange.calledWith(args)).to.equal(true);
74 |
75 | wrapper.setProps({ value: args.value });
76 |
77 | wrapper.find('Toggle').at(0).simulate('click');
78 |
79 | const args2 = {
80 | name: 'change',
81 | value: [{
82 | id:1,
83 | label: 'a',
84 | count: 6,
85 | value: true
86 | }, {
87 | id:2,
88 | label: 'b',
89 | value: true
90 | }, {
91 | id:3,
92 | label: 'c'
93 | }],
94 | oldValue: [{
95 | id:1,
96 | label: 'a',
97 | count: 6
98 | }, {
99 | id:2,
100 | label: 'b',
101 | value: true
102 | }, {
103 | id:3,
104 | label: 'c'
105 | }],
106 | index: 0,
107 | selectedIds: [1, 2]
108 | };
109 |
110 | expect(onChange.calledWith(args2)).to.equal(true);
111 | });
112 |
113 | it('should behave like a radio button when type is radio', () => {
114 | const onChange = sinon.spy();
115 |
116 | const wrapper = mount(
117 |
123 | );
124 |
125 | wrapper.find('Toggle').at(1).simulate('click');
126 |
127 | const args = {
128 | name: 'radio-group',
129 | value: [{
130 | id:1,
131 | label: 'a',
132 | count: 6,
133 | value: false
134 | }, {
135 | id:2,
136 | label: 'b',
137 | value: true
138 | }, {
139 | id:3,
140 | label: 'c',
141 | value: false
142 | }],
143 | oldValue: value,
144 | index: 1,
145 | selectedIds: 2
146 | };
147 |
148 | expect(onChange.calledWith(args)).to.equal(true);
149 |
150 | wrapper.setProps({ value: args.value });
151 |
152 | wrapper.find('Toggle').at(0).simulate('click');
153 |
154 | const args2 = {
155 | name: 'radio-group',
156 | value: [{
157 | id:1,
158 | label: 'a',
159 | count: 6,
160 | value: true
161 | }, {
162 | id:2,
163 | label: 'b',
164 | value: false
165 | }, {
166 | id:3,
167 | label: 'c',
168 | value: false
169 | }],
170 | oldValue: args.value,
171 | index: 0,
172 | selectedIds: 1
173 | };
174 |
175 | expect(onChange.calledWith(args2)).to.equal(true);
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/components/Slider/Slider.scss:
--------------------------------------------------------------------------------
1 | @import "../theme";
2 | @import "../mixins";
3 |
4 | .rf-slider{
5 | overflow: hidden;
6 | padding: 0 10px;
7 | &.slider-vertical{
8 | overflow: visible;
9 | }
10 | }
11 |
12 | .slider-wrapper {
13 | width: 100%;
14 | overflow: visible;
15 | position: relative;
16 | display: inline-block;
17 | padding-top: 40px;
18 |
19 | .slider-vertical & {
20 | width: $slider-track-width;
21 | height: 400px;
22 | padding-top: 0;
23 | }
24 | }
25 |
26 | .slider-vertical {
27 | width: 20px;
28 | height: 400px;
29 | position: relative;
30 |
31 | .slider-track {
32 | position: absolute;
33 | }
34 | }
35 |
36 | .slider-track, .slider-rail {
37 | height: $slider-track-width;
38 | background-color: $secondary-color;
39 | border-radius: $slider-track-width/2;
40 |
41 | .slider-vertical & {
42 | width: $slider-track-width;
43 | height: 400px;
44 | }
45 | }
46 |
47 | .slider-slider-wrapper {
48 | will-change: transform, top;
49 | transition: transform .2s ease-out, top .2s ease-out;
50 | transform: translate3d(0, 0, 0);
51 | position: relative;
52 | }
53 |
54 | .slider-control {
55 | height: $slider-control-width;
56 | width: $slider-control-width;
57 | border-radius: $slider-control-width/2;
58 | background-color: darken($primary-color, 10%);
59 | position: absolute;
60 | display: inline-block;
61 | vertical-align: top;
62 | margin-top: -($slider-control-width + $slider-track-width)/2;
63 | margin-left: -$slider-control-width/2;
64 | cursor: pointer;
65 | transition: transform .2s ease-out;
66 |
67 | .slider-disabled & {
68 | background-color: grayscale(darken($primary-color, 10%));
69 | }
70 |
71 | .slider-vertical & {
72 | margin-left: -($slider-control-width/2 - $slider-track-width/2);
73 | margin-top: -$slider-control-width/2;
74 | }
75 | }
76 |
77 | .slider-rail {
78 | position: relative;
79 | background-color: $primary-color;
80 | transition: all .2s ease-out;
81 |
82 | .slider-disabled & {
83 | background-color: grayscale($primary-color);
84 | }
85 | }
86 |
87 | .slider-active .slider-slider {
88 | transform: scale(1.4) translate3d(0, 0, 0);
89 | }
90 |
91 | // TODO : prevent text selection in disabled state
92 |
93 | .slider-value {
94 | position: absolute;
95 | width: 40px;
96 | margin-left: -20px;
97 | text-align: center;
98 | margin-top: - ($slider-track-width + $slider-control-width/2 + 20px);
99 | @include no-select();
100 |
101 | .slider-disabled & {
102 | color: darken($secondary-color, 20%);
103 | }
104 |
105 | .slider-vertical & {
106 | margin-top: -($slider-control-width/2);
107 | margin-left: -40px;
108 | }
109 | }
110 |
111 | .slider-range {
112 | @include no-select();
113 |
114 | div {
115 | display: inline-block;
116 | margin-top: 10px;
117 |
118 | &.slider-range-max {
119 | float: right;
120 | }
121 | }
122 |
123 | .slider-vertical & {
124 | margin-left: 20px;
125 | div{
126 | margin-top: 0;
127 | }
128 | position: absolute;
129 | top: 0;
130 | height: 100%;
131 |
132 | .slider-range-min{
133 | position: absolute;
134 | bottom: 0;
135 | margin-bottom: -10px;
136 | }
137 |
138 | .slider-range-max{
139 | margin-top: -10px;
140 | }
141 | }
142 | }
143 |
144 | $stepWidth: 6px;
145 |
146 | .slider-steps-wrapper {
147 | width: 100%;
148 | height: $slider-track-width;
149 | margin-top: -$slider-track-width;
150 | position: absolute;
151 |
152 | .slider-vertical & {
153 | height: 100%;
154 | margin-top: -($slider-step-width + 4px)/2;
155 | }
156 | }
157 |
158 | .slider-step {
159 | width: $slider-step-width;
160 | height: $slider-step-width;
161 | background-color: white;
162 | display: inline-block;
163 | position: absolute;
164 | margin-top: -2px;
165 | border-radius: 50%;
166 | border: 2px solid $secondary-color;
167 | margin-left: -($slider-step-width+4px)/2;
168 | will-change: border-color;
169 | transition: border-color .2s ease-out;
170 |
171 | &.slider-step-active {
172 | border-color: $primary-color;
173 | }
174 |
175 | .slider-vertical & {
176 | margin-left: -2px;
177 | margin-top: 0;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/test/Toggle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | const { describe, it, __base } = global;
4 | import { shallow } from 'enzyme';
5 | import sinon from 'sinon';
6 | import { expect } from 'chai';
7 |
8 | const { Toggle } = require(`${__base}components`);
9 |
10 | function noop () {
11 |
12 | }
13 |
14 | describe('Toggle Component', () => {
15 | it('should call onChange function on click', () => {
16 | const onChange = sinon.spy();
17 |
18 | const wrapper = shallow(
19 |
25 | );
26 |
27 | wrapper.simulate('click');
28 |
29 | expect(onChange.called).to.equal(true);
30 | });
31 |
32 | it('should call onChange function with correct arguments', () => {
33 | const onChange = sinon.spy();
34 |
35 | const wrapper = shallow(
36 |
42 | );
43 |
44 | wrapper.simulate('click');
45 |
46 | const args = {
47 | name: 'switch',
48 | value: true
49 | };
50 |
51 | expect(onChange.calledWith(args)).to.equal(true);
52 | });
53 |
54 | it('should change class to toggle-on on click', () => {
55 | const onChange = sinon.spy();
56 |
57 | const wrapper = shallow(
58 |
64 | );
65 |
66 | wrapper.simulate('click');
67 |
68 | expect(wrapper.closest('.rf-active')).to.have.length(1);
69 | });
70 |
71 | it('should render the count', () => {
72 | const onChange = sinon.spy();
73 |
74 | const wrapper = shallow(
75 |
83 | );
84 |
85 | expect(wrapper.find('span.toggle-count')).to.have.length(1);
86 | });
87 |
88 | it('should render checkbox when checkbox option is passed', () => {
89 | const onChange = sinon.spy();
90 |
91 | const wrapper = shallow(
92 |
98 | );
99 |
100 | expect(wrapper.find('i.icon-check-box-outline-blank')).to.have.length(1);
101 |
102 | wrapper.simulate('click');
103 |
104 | expect(onChange.calledOnce).to.equal(true);
105 | });
106 |
107 | it('should render radio button when radio option is passed', () => {
108 | const onChange = sinon.spy();
109 |
110 | const wrapper = shallow(
111 |
116 | );
117 |
118 | expect(wrapper.find('i.icon-radio-button-unchecked')).to.have.length(1);
119 |
120 | wrapper.simulate('click');
121 |
122 | expect(onChange.calledOnce).to.equal(true);
123 | });
124 |
125 | it('should render label when passed', () => {
126 | const onChange = sinon.spy();
127 |
128 | const wrapper = shallow(
129 |
135 | );
136 |
137 | expect(wrapper.find('.toggle-icon-label').text()).to.equal('off');
138 |
139 | wrapper.setProps({ value: true });
140 |
141 | expect(wrapper.find('.toggle-icon-label').text()).to.equal('on');
142 | });
143 |
144 | it('should not call render if both updated value and count are same', () => {
145 | const onChange = function () {
146 | };
147 |
148 | const render = sinon.spy(Toggle.prototype, 'render');
149 |
150 | const wrapper = shallow(
151 |
158 | );
159 |
160 | expect(render.calledOnce).to.equal(true);
161 | wrapper.setProps({
162 | value: true,
163 | count: 5
164 | });
165 |
166 | expect(render.calledTwice).to.equal(false);
167 |
168 | wrapper.setProps({
169 | value: true,
170 | count: 6
171 | });
172 |
173 | expect(render.calledTwice).to.equal(true);
174 | });
175 |
176 | it('should render the icon element if passed', () => {
177 | const onChange = noop;
178 |
179 | const iconElement = function (p) {
180 | const iconClass = classNames({
181 | 'icon-not-selected': !p.value,
182 | 'icon-selected': p.value
183 | });
184 | return ;
185 | };
186 |
187 | const wrapper = shallow(
188 |
194 | );
195 |
196 | expect(wrapper.find('.icon-not-selected')).has.length(1);
197 |
198 | wrapper.setProps({ value: true });
199 |
200 | expect(wrapper.find('.icon-selected')).has.length(1);
201 | });
202 | });
203 |
--------------------------------------------------------------------------------
/components/Slider/Control.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classNames from 'classnames';
3 | import { hasStepDifference, suppress, isWithinRange, removeClass, isVertical } from './utils';
4 | import { getValueFromPosition, getRelativePosition, getPositionFromValue } from './helpers';
5 | import autoBind from '../utils/autoBind';
6 |
7 | import constants from './constants';
8 |
9 | export default class Control extends Component {
10 | constructor (props, context) {
11 | super(props, context);
12 |
13 | autoBind([
14 | 'handleMouseDown',
15 | 'handleDrag',
16 | 'handleMouseUp',
17 | 'handleTouchStart',
18 | 'handleTouchEnd',
19 | 'onChange'
20 | ], this);
21 | }
22 |
23 | componentDidMount () {
24 | this.setSliderPosition(this.props);
25 | this.controlWidth = this.getControlWidth();
26 | }
27 |
28 | componentWillReceiveProps (newProps) {
29 | const propsChanged = (newProps.value !== this.props.value) ||
30 | (newProps.trackOffset.width !== this.props.trackOffset.width);
31 | if (propsChanged) this.setSliderPosition(newProps);
32 | }
33 |
34 | shouldComponentUpdate (newProps) {
35 | const dimension = constants[newProps.orientation].dimension;
36 |
37 | return (
38 | (hasStepDifference(newProps.value, this.props.value, newProps.step) &&
39 | isWithinRange(newProps, newProps.value)) ||
40 | newProps.trackOffset[dimension] !== this.props.trackOffset[dimension]
41 | );
42 | }
43 |
44 | onChange (value, isRenderRequired = false) {
45 | this.props.onChange({
46 | controlWidth: this.controlWidth,
47 | name: this.props.name,
48 | value
49 | }, isRenderRequired);
50 | }
51 |
52 | getControlWidth () {
53 | const control = this.refs.control;
54 | if (!control) return 0;
55 | return control.offsetWidth;
56 | }
57 |
58 | setSliderPosition (props) {
59 | const { value } = props;
60 | this.onChange(value, true);
61 | }
62 |
63 | handleMouseDown (e) {
64 | suppress(e);
65 | this.refs.controlWrapper.className += ' slider-active';
66 | document.addEventListener('mouseup', this.handleMouseUp);
67 | if (this.props.readOnly) return;
68 |
69 | document.addEventListener('mousemove', this.handleDrag);
70 | this.props.onDragExtreme(this.props.name, this.props.value, 'start');
71 | }
72 |
73 | handleMouseUp (e) {
74 | suppress(e);
75 | this.refs.controlWrapper.className = removeClass(this.refs.controlWrapper, 'slider-active');
76 | document.removeEventListener('mouseup', this.handleMouseUp);
77 |
78 | if (this.props.readOnly) return;
79 |
80 | document.removeEventListener('mousemove', this.handleDrag);
81 | this.props.onDragExtreme(this.props.name, this.newValue, 'end');
82 | }
83 |
84 | handleTouchStart () {
85 | document.addEventListener('touchmove', this.handleDrag);
86 | document.addEventListener('touchend', this.handleTouchEnd);
87 | this.props.onDragExtreme(this.props.name, this.props.value, 'start');
88 | }
89 |
90 | handleTouchEnd () {
91 | document.removeEventListener('touchmove', this.handleDrag);
92 | document.removeEventListener('touchend', this.handleTouchEnd);
93 | this.props.onDragExtreme(this.props.name, this.newValue, 'end');
94 | }
95 |
96 | handleDrag (e) {
97 | suppress(e);
98 | const position = getRelativePosition(e, this.props, this.controlWidth);
99 |
100 | this.newValue = getValueFromPosition(this.props, position);
101 | this.onChange(this.newValue);
102 | }
103 |
104 | render () {
105 | const { name, value, toolTipTemplate, disabled, orientation } = this.props;
106 |
107 | const className = classNames('slider-control', name);
108 |
109 | const sliderPosition = isVertical(orientation) ?
110 | (100 - getPositionFromValue(this.props)) : getPositionFromValue(this.props);
111 |
112 | let style;
113 |
114 | if (isVertical(orientation)) {
115 | style = {
116 | top: `${sliderPosition}%`
117 | };
118 | } else {
119 | style = {
120 | transform: `translateX(${sliderPosition}%) translate3d(0,0,0)`
121 | };
122 | }
123 |
124 |
125 | return (
126 |
127 |
128 | {toolTipTemplate(value)}
129 |
130 |
137 |
138 | );
139 | }
140 | }
141 |
142 | Control.propTypes = {
143 | disabled: PropTypes.bool.isRequired,
144 | max: PropTypes.number.isRequired,
145 | min: PropTypes.number.isRequired,
146 | name: PropTypes.string.isRequired,
147 | onChange: PropTypes.func.isRequired,
148 | onDragExtreme: PropTypes.func.isRequired,
149 | orientation: PropTypes.string.isRequired,
150 | readOnly: PropTypes.bool.isRequired,
151 | step: PropTypes.number.isRequired,
152 | toolTipTemplate: PropTypes.func.isRequired,
153 | trackOffset: PropTypes.object.isRequired,
154 | value: PropTypes.number.isRequired
155 | };
156 |
--------------------------------------------------------------------------------
/components/Toggle/Toggle.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import classNames from 'classnames';
3 | import autoBind from '../utils/autoBind';
4 | import noop from '../utils/noop';
5 |
6 | function radioElement (p) {
7 | const iconClass = classNames({
8 | 'icon-radio-button-unchecked': !p.value,
9 | 'icon-radio-button-checked': p.value
10 | });
11 | return ;
12 | }
13 |
14 | function checkBoxElement (p) {
15 | const iconClass = classNames({
16 | 'icon-check-box-outline-blank': !p.value,
17 | 'icon-check-box': p.value
18 | });
19 | return ;
20 | }
21 |
22 | function switchElement (prop) {
23 | const labelClass = classNames('toggle-icon-label', {
24 | 'toggle-il-left': prop.value,
25 | 'toggle-il-right': !prop.value
26 | });
27 |
28 | let iconLabelText;
29 |
30 | if (prop.iconLabel && prop.iconLabel.length) {
31 | iconLabelText = prop.value ? prop.iconLabel[0] : prop.iconLabel[1];
32 | }
33 | return (
34 |
35 |
{iconLabelText}
36 |
37 |
38 | );
39 | }
40 |
41 | /**
42 | * Hello world
43 | */
44 | export default class Toggle extends React.Component {
45 | constructor (props) {
46 | super(props);
47 | autoBind([
48 | 'handleClick'
49 | ], this);
50 | }
51 |
52 | shouldComponentUpdate (nextProps) {
53 | return (
54 | (nextProps.value !== this.props.value) ||
55 | (nextProps.count !== this.props.count)
56 | );
57 | }
58 |
59 | getIconElement () {
60 | const { iconElement, type } = this.props;
61 | if (typeof iconElement === 'function') return iconElement(this.props);
62 | if (type === 'radio') return radioElement(this.props);
63 | else if (type === 'checkbox') return checkBoxElement(this.props);
64 | else return switchElement(this.props);
65 | }
66 |
67 | handleClick () {
68 | this.props.onChange({
69 | name: this.props.name,
70 | value: !this.props.value
71 | });
72 | }
73 |
74 | isNormal () {
75 | return this.props.mode === 'normal';
76 | }
77 |
78 | render () {
79 | const {
80 | attributes,
81 | className,
82 | name,
83 | label,
84 | labelPosition,
85 | value,
86 | disabled,
87 | countElem,
88 | count,
89 | type
90 | } = this.props;
91 |
92 | const mainClass = classNames('rf-toggle', type, className, name, {
93 | 'toggle-disabled': disabled,
94 | 'toggle-active': value,
95 | 'toggle-tag': !this.isNormal()
96 | });
97 |
98 | const labelClass = classNames('toggle-label', {
99 | 'toggle-before': labelPosition === 'before',
100 | 'toggle-after': labelPosition === 'after'
101 | });
102 |
103 | return (
104 |
109 | {
110 | label &&
111 | {label}
112 | {count !== undefined && countElem(this.props)}
113 |
114 | }
115 | {this.isNormal() && this.getIconElement()}
116 |
117 | );
118 | }
119 | }
120 |
121 | Toggle.propTypes = {
122 | /**
123 | * Sometimes you may need to add some custom attributes to the root tag of the
124 | * component. attributes will accept an object where the key and values will
125 | * be those attributes and their value respectively.
126 | *
127 | * Eg : If you pass
128 | * ```js
129 | * attributes = {
130 | * 'data-attr1' : 'val1',
131 | * 'data-attr2' : 'val2'
132 | * }
133 | * ```
134 | * the root tag will have the attributes `data-attr1` and `data-attr2` with the
135 | * corresponding values as `val1` and `val2` respectively
136 | */
137 | attributes: PropTypes.object,
138 |
139 | /**
140 | * Optional className to be added to the root tag of the component
141 | */
142 | className: PropTypes.string,
143 |
144 | /**
145 | * In case you want to show aggregation/count in front of label then pass the
146 | * number in this option. This is generally useful for showing the items present
147 | * corresponding to that filter option.
148 | */
149 | count: PropTypes.number,
150 | countElem: PropTypes.oneOfType([
151 | PropTypes.func,
152 | PropTypes.element
153 | ]),
154 |
155 | /**
156 | * Set to `true` if you want to disable the component interactions.
157 | */
158 | disabled: PropTypes.bool,
159 | iconElement: PropTypes.func,
160 | iconLabel: PropTypes.array,
161 |
162 | /**
163 | * The label text present in the component. If this option is not set only the
164 | * icon element will render.
165 | */
166 | label: PropTypes.string,
167 | labelPosition: PropTypes.oneOf([
168 | 'before', 'after'
169 | ]),
170 | mode: PropTypes.oneOf(['normal', 'tag']),
171 | name: PropTypes.string.isRequired,
172 | onChange: PropTypes.func,
173 | type: PropTypes.oneOf([
174 | 'switch', 'radio', 'checkbox'
175 | ]),
176 | value: PropTypes.bool.isRequired
177 | };
178 |
179 | Toggle.defaultProps = {
180 | countElem (p) {
181 | return ({p.count});
182 | },
183 | disabled: false,
184 | labelPosition: 'before',
185 | mode: 'normal',
186 | onChange: noop,
187 | type: 'switch',
188 | value: false
189 | };
190 |
--------------------------------------------------------------------------------
/components/Slider/Slider.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 | import { isWithinRange, suppress, isEqual, isVertical, formatValue, capitalize } from './utils';
4 | import { getNearestValue } from './helpers';
5 | import Control from './Control';
6 | import Steps from './Steps';
7 | import Rail from './Rail';
8 | import autoBind from '../utils/autoBind';
9 | import noop from '../utils/noop';
10 |
11 | export default class Slider extends Component {
12 | constructor (props) {
13 | super(props);
14 |
15 | this.state = {
16 | trackOffset: {}
17 | };
18 |
19 | autoBind([
20 | 'onChange',
21 | 'onControlChange',
22 | 'handleClick',
23 | 'updatePosition',
24 | 'onDragExtreme'
25 | ], this);
26 | }
27 |
28 | componentDidMount () {
29 | this.updatePosition();
30 | window.addEventListener('resize', this.updatePosition);
31 | }
32 |
33 | shouldComponentUpdate (newProps, newState) {
34 | return (
35 | isWithinRange(newProps, newProps.value) &&
36 | (!isEqual(this.props.value, newProps.value) || !!this.isRerenderRequired ||
37 | this.state.trackOffset.width !== newState.trackOffset.width)
38 | );
39 | }
40 |
41 | componentDidUpdate () {
42 | this.isRerenderRequired = false;
43 | }
44 |
45 | componentWillUnmount () {
46 | window.removeEventListener('resize', this.updatePosition);
47 | }
48 |
49 | onChange (value, changed) {
50 | const args = {
51 | name: this.props.name,
52 | value
53 | };
54 |
55 | if (changed) args.changed = changed;
56 | this.props.onChange(args);
57 | }
58 |
59 | onControlChange (data, isRenderRequired) {
60 | let value;
61 | if (this.isRangeType()) {
62 | value = formatValue(this.props.value, data.value, data.name, this.props.type);
63 | } else {
64 | value = data.value;
65 | }
66 |
67 | // only trigger on first onChange trigger
68 | this.isRerenderRequired = isRenderRequired;
69 |
70 | if (isWithinRange(this.props, value) && !isEqual(this.props.value, value)) {
71 | this.onChange(value, data.name);
72 | }
73 | }
74 |
75 | onDragExtreme (name, value, action) {
76 | const newValue = formatValue(this.props.value, value, name, this.props.type);
77 | this.props[`onDrag${capitalize(action)}`]({
78 | changed: name,
79 | name: this.props.name,
80 | value: newValue
81 | });
82 | }
83 |
84 | getControl (value, name) {
85 | const {
86 | step,
87 | orientation,
88 | min,
89 | max,
90 | readOnly,
91 | disabled,
92 | toolTipTemplate
93 | } = this.props;
94 |
95 | return (
96 |
110 | );
111 | }
112 |
113 | getTrackOffset () {
114 | return this.state.trackOffset;
115 | }
116 |
117 | updatePosition () {
118 | const track = this.refs.track;
119 |
120 | setTimeout(() => {
121 | window.requestAnimationFrame(() => {
122 | this.setState({
123 | trackOffset: track ? track.getBoundingClientRect() : {}
124 | });
125 | });
126 | }, 0);
127 | }
128 |
129 | handleClick (e) {
130 | suppress(e);
131 | const newData = getNearestValue(e, this.props, this.getTrackOffset());
132 | this.onChange(newData.value, newData.changed);
133 | this.onDragExtreme(newData.changed, newData.value, 'end');
134 | }
135 |
136 | isRangeType () {
137 | return this.props.type === 'range';
138 | }
139 |
140 | render () {
141 | const {
142 | name,
143 | disabled,
144 | step,
145 | min,
146 | max,
147 | value,
148 | rangeTemplate,
149 | showSteps,
150 | orientation,
151 | attributes
152 | } = this.props;
153 |
154 | const mainClass = classNames('react-filters', 'rf-slider', name, {
155 | 'slider-disabled': disabled,
156 | 'slider-vertical': isVertical(orientation)
157 | });
158 |
159 | const lowerValue = this.isRangeType() ? value[0] : value;
160 |
161 | return (
162 |
163 |
164 |
169 |
176 |
177 |
178 | {
179 | showSteps &&
188 | }
189 |
190 | {this.getControl(lowerValue, 'lower')}
191 | {this.isRangeType() && this.getControl(value[1], 'upper')}
192 |
193 |
194 | {rangeTemplate(min, max)}
195 |
196 | );
197 | }
198 | }
199 |
200 | Slider.propTypes = {
201 | attributes: PropTypes.object,
202 | disabled: PropTypes.bool,
203 | max: PropTypes.number,
204 | min: PropTypes.number,
205 | name: PropTypes.string.isRequired,
206 | onChange: PropTypes.func,
207 | onDragEnd: PropTypes.func,
208 | onDragStart: PropTypes.func,
209 | orientation: PropTypes.oneOf(['horizontal', 'vertical']),
210 | rangeTemplate: PropTypes.func,
211 | readOnly: PropTypes.bool,
212 | showSteps: PropTypes.bool,
213 | step: PropTypes.number,
214 | toolTipTemplate: PropTypes.func,
215 | type: PropTypes.oneOf(['value', 'range']),
216 | value: PropTypes.oneOfType([PropTypes.array, PropTypes.number])
217 | };
218 |
219 | Slider.defaultProps = {
220 | attributes: {},
221 | disabled: false,
222 | max: 20,
223 | min: 0,
224 | onDragEnd: noop,
225 | onDragStart: noop,
226 | orientation: 'horizontal',
227 | rangeTemplate (min, max) {
228 | return (
229 |
230 |
{min}
231 |
{max}
232 |
233 | );
234 | },
235 | readOnly: false,
236 | showSteps: false,
237 | step: 1,
238 | toolTipTemplate (value) {
239 | return value;
240 | },
241 | type: 'value',
242 | value: [5, 10]
243 | };
244 |
--------------------------------------------------------------------------------
/components/AutoComplete/AutoComplete.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classNames from 'classnames';
3 | import Fuzzy from 'fuse.js';
4 | import autoBind from '../utils/autoBind';
5 | import debounce from '../utils/debounce';
6 | import SearchBox from './SearchBox';
7 | import Suggestions from './Suggestions';
8 | import deepCopy from 'deep-copy';
9 |
10 | export default class AutoComplete extends Component {
11 | constructor (props) {
12 | super(props);
13 |
14 | this.state = {
15 | multiSelected: [],
16 | query: '',
17 | results: props.showInitialResults ? props.list : [],
18 | selectedIndex: 0
19 | };
20 |
21 | autoBind([
22 | 'onSelect',
23 | 'onKeyDown',
24 | 'getOptions',
25 | 'onResetClick',
26 | 'handleQueryChange',
27 | 'removeTag'
28 | ], this);
29 |
30 | this.handleChange = debounce(this.handleChange, props.debounce);
31 | if (!props.async) this.fuse = new Fuzzy(props.list, this.getOptions());
32 | }
33 |
34 | componentWillReceiveProps (newProps, newState) {
35 | if (newProps.async || (newProps.showInitialResults && !newState.query)) {
36 | this.setState({ results: newProps.list || [] });
37 | }
38 | }
39 |
40 | onSelect () {
41 | const { name, onSelect } = this.props;
42 | onSelect(name);
43 | }
44 |
45 | onKeyDown (e) {
46 | const { selectedIndex, results } = this.state;
47 | const { name, valueKey, onSelect, multiSelect } = this.props;
48 |
49 | if (e.keyCode === 40 && (selectedIndex < results.length - 1)) {
50 | this.setState({
51 | selectedIndex: selectedIndex + 1
52 | });
53 | } else if (e.keyCode === 38 && (selectedIndex > 0)) {
54 | this.setState({
55 | selectedIndex: selectedIndex - 1
56 | });
57 | } else if (e.keyCode === 13) {
58 | if (multiSelect) {
59 | this.state.multiSelected.push(results[selectedIndex]);
60 | }
61 |
62 | if (multiSelect) {
63 | onSelect({
64 | action: 'added',
65 | changed: results[selectedIndex],
66 | name,
67 | value: this.state.multiSelected
68 | });
69 | } else if (results[selectedIndex]) {
70 | onSelect({
71 | name,
72 | value: results[selectedIndex]
73 | });
74 | }
75 |
76 | this.setState({
77 | query: multiSelect ? '' : results[selectedIndex][valueKey],
78 | results: [],
79 | selectedIndex: 0
80 | });
81 | }
82 | }
83 |
84 | onResetClick () {
85 | this.setState({ query: '' });
86 | if (!this.props.showInitialResults) this.setState({ results: [] });
87 | }
88 |
89 | getOptions () {
90 | return { ...this.props.fuzzyOptions, keys: this.props.keys };
91 | }
92 |
93 | getSuggestions () {
94 | const { resultsTemplate } = this.props;
95 | const { results, selectedIndex } = this.state;
96 | if (results && results.length) {
97 | return (
98 |
103 | );
104 | }
105 | return null;
106 | }
107 |
108 | removeTag ({ id }) {
109 | const changed = this.state.multiSelected[id];
110 | const multiSelected = deepCopy(this.state.multiSelected);
111 | multiSelected.splice(id, 1);
112 | this.setState({ multiSelected }, () => (
113 | this.props.onSelect({
114 | action: 'removed',
115 | changed,
116 | name: this.props.name,
117 | value: this.state.multiSelected
118 | })
119 | ));
120 | }
121 |
122 | handleQueryChange (query) {
123 | if (!this.props.async) {
124 | this.setState({
125 | query,
126 | results: this.props.showInitialResults && !query ? this.props.list : this.fuse.search(query)
127 | });
128 | }
129 |
130 | if (typeof this.props.onChange === 'function') {
131 | this.setState({
132 | query,
133 | results: this.props.onChange(query, this.props, this)
134 | });
135 | }
136 | }
137 |
138 | render () {
139 | const {
140 | name,
141 | disabled,
142 | placeholder,
143 | onFocus,
144 | onBlur,
145 | Reset,
146 | multiSelect,
147 | showTagRemove,
148 | valueKey
149 | } = this.props;
150 |
151 | const mainClass = classNames('react-filters', 'rf-autocomplete', name, {
152 | disabled
153 | });
154 |
155 | return (
156 |
157 |
172 |
173 | {this.getSuggestions()}
174 |
175 | );
176 | }
177 | }
178 |
179 | AutoComplete.propTypes = {
180 | Reset: PropTypes.func,
181 | async: PropTypes.bool,
182 | className: PropTypes.string,
183 | debounce: PropTypes.number,
184 | disabled: PropTypes.bool,
185 | list: PropTypes.array,
186 | multiSelect: PropTypes.bool,
187 | name: PropTypes.string.isRequired,
188 | onBlur: PropTypes.func,
189 | onChange: PropTypes.func,
190 | onFocus: PropTypes.func,
191 | onSelect: PropTypes.func.isRequired,
192 | placeholder: PropTypes.string,
193 | resultsTemplate: PropTypes.func,
194 | showInitialResults: PropTypes.bool,
195 | showTagRemove: PropTypes.bool,
196 | valueKey: PropTypes.string,
197 | width: PropTypes.number,
198 | fuzzyOptions: PropTypes.shape({
199 | caseSensitive: PropTypes.bool,
200 | id: PropTypes.string,
201 | include: PropTypes.array,
202 | keys: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
203 | shouldSort: PropTypes.bool,
204 | sortFn: PropTypes.func,
205 | tokenize: PropTypes.bool,
206 | verbose: PropTypes.bool,
207 | maxPatternLength: PropTypes.number,
208 | distance: PropTypes.number,
209 | threshold: PropTypes.number,
210 | location: PropTypes.number
211 | })
212 | };
213 |
214 | AutoComplete.defaultProps = {
215 | async: false,
216 | debounce: 250,
217 | disabled: false,
218 | multiSelect: false,
219 | placeholder: 'Search',
220 | resultsTemplate: Suggestions.defaultResultsTemplate,
221 | showInitialResults: false,
222 | showTagRemove: true,
223 | tags: false,
224 | valueKey: 'title',
225 | width: 430,
226 | fuzzyOptions: {
227 | caseSensitive: false,
228 | shouldSort: true,
229 | sortFn (a, b) {
230 | return a.score - b.score;
231 | },
232 | threshold: 0.6,
233 | tokenize: false,
234 | verbose: false,
235 | distance: 100,
236 | include: [],
237 | location: 0
238 | }
239 | };
240 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [](https://github.com/ritz078/react-filters/compare/v0.2.1...v) (2016-09-06)
3 |
4 |
5 | ### Performance Improvements
6 |
7 | * **build:** decreased filesize by removing webpack code from build files ([cb927ff](https://github.com/ritz078/react-filters/commit/cb927ff))
8 |
9 |
10 |
11 |
12 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-11)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * **group:** accept and return selectedIds as number if type is radio ([b5c67ee](https://github.com/ritz078/react-filters/commit/b5c67ee))
18 |
19 |
20 | ### Features
21 |
22 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0))
23 |
24 |
25 |
26 |
27 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-11)
28 |
29 |
30 | ### Features
31 |
32 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0))
33 |
34 |
35 |
36 |
37 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-10)
38 |
39 |
40 | ### Features
41 |
42 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0))
43 |
44 |
45 |
46 |
47 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-10)
48 |
49 |
50 | ### Features
51 |
52 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0))
53 |
54 |
55 |
56 |
57 | ## [0.1.6](https://github.com/ritz078/react-filters/compare/v0.1.5...v0.1.6) (2016-08-10)
58 |
59 |
60 |
61 |
62 | ## [0.1.5](https://github.com/ritz078/react-filters/compare/v0.1.4...v0.1.5) (2016-08-09)
63 |
64 |
65 |
66 |
67 | ## [0.1.4](https://github.com/ritz078/react-filters/compare/v0.1.3...v0.1.4) (2016-08-09)
68 |
69 |
70 |
71 |
72 | ## [0.1.3](https://github.com/ritz078/react-filters/compare/v0.1.2...v0.1.3) (2016-08-09)
73 |
74 |
75 |
76 |
77 | ## [0.1.2](https://github.com/ritz078/react-filters/compare/v0.1.1...v0.1.2) (2016-08-09)
78 |
79 |
80 |
81 |
82 | ## [0.1.1](https://github.com/ritz078/react-filters/compare/v0.1.0...v0.1.1) (2016-08-09)
83 |
84 |
85 |
86 |
87 | # [0.1.0](https://github.com/ritz078/react-filters/compare/5102f55...v0.1.0) (2016-08-09)
88 |
89 |
90 | ### Bug Fixes
91 |
92 | * **eslint:** added eslint loader ([7e39fe6](https://github.com/ritz078/react-filters/commit/7e39fe6))
93 | * **eslint:** fixed the order of loader ([8cc1986](https://github.com/ritz078/react-filters/commit/8cc1986))
94 | * **hmr:** css also live reloading in dev environment ([85ec692](https://github.com/ritz078/react-filters/commit/85ec692))
95 | * **ignore:** added npm-debug-log ([dd456e9](https://github.com/ritz078/react-filters/commit/dd456e9))
96 | * **mocha/document:** resolve eslint single-string error ([5102f55](https://github.com/ritz078/react-filters/commit/5102f55))
97 | * **range:** initial rail render is now fixed ([2502cee](https://github.com/ritz078/react-filters/commit/2502cee))
98 | * **range:** window resize event working as expected. ([8266296](https://github.com/ritz078/react-filters/commit/8266296))
99 | * **slider:** now working fine for vertical range type sliders ([d366bbe](https://github.com/ritz078/react-filters/commit/d366bbe))
100 | * **slider:** on first render track width wasn't calculated right ([531af63](https://github.com/ritz078/react-filters/commit/531af63))
101 | * **slider:** value passed for range slider corrected ([60dffa4](https://github.com/ritz078/react-filters/commit/60dffa4))
102 | * **switch:** no more sass, inline CSS implemented and switch is now working ([0176b28](https://github.com/ritz078/react-filters/commit/0176b28))
103 | * **switch:** remove label div if label isn't passed in props ([130e5c4](https://github.com/ritz078/react-filters/commit/130e5c4))
104 | * **switch:** separated styles into separate SCSS file ([8d3d78c](https://github.com/ritz078/react-filters/commit/8d3d78c))
105 | * **test:** removed start script and fixed test files path ([36774e4](https://github.com/ritz078/react-filters/commit/36774e4))
106 | * **tests:** and also removed cover task from ghook ([dfc45e5](https://github.com/ritz078/react-filters/commit/dfc45e5))
107 | * **webpack:** added webpack middleware for storybook ([5a137fc](https://github.com/ritz078/react-filters/commit/5a137fc))
108 |
109 |
110 | ### Features
111 |
112 | * **autocomplete:** added fuzzy search and async requests in autocomplete ([26d2216](https://github.com/ritz078/react-filters/commit/26d2216))
113 | * **autocomplete:** show the suggesstions on load ([ccc52ba](https://github.com/ritz078/react-filters/commit/ccc52ba))
114 | * **autocomplete:** trigger onSelect on removal of tags ([33abcc5](https://github.com/ritz078/react-filters/commit/33abcc5))
115 | * **Count:** added count filter with its tests. ([79cb34c](https://github.com/ritz078/react-filters/commit/79cb34c))
116 | * **css-modules:** added support for css modules loader with sass integration ([a5d389c](https://github.com/ritz078/react-filters/commit/a5d389c))
117 | * **ghooks:** added ghooks and fixed linting errors ([0c1b6e8](https://github.com/ritz078/react-filters/commit/0c1b6e8))
118 | * **group:** added group component and combined switch, radio and checkbox ([c38c022](https://github.com/ritz078/react-filters/commit/c38c022))
119 | * **group:** added old value in the onChange ([23b62fa](https://github.com/ritz078/react-filters/commit/23b62fa))
120 | * **hmr:** enable hmr and setup a demo page ([c0ba9eb](https://github.com/ritz078/react-filters/commit/c0ba9eb))
121 | * **InputRange:** started input range and fixed linting errors ([ebf85dc](https://github.com/ritz078/react-filters/commit/ebf85dc))
122 | * **radio:** started project, implemented react-storybook ([ee43572](https://github.com/ritz078/react-filters/commit/ee43572))
123 | * **range:** added on click event handler on the track ([791f43f](https://github.com/ritz078/react-filters/commit/791f43f))
124 | * **range:** added readOnly and disabled state ([2495823](https://github.com/ritz078/react-filters/commit/2495823))
125 | * **range:** added value and range indicator ([ca17778](https://github.com/ritz078/react-filters/commit/ca17778))
126 | * **range:** fixed steps increment issue and refactored code ([41fc6f8](https://github.com/ritz078/react-filters/commit/41fc6f8))
127 | * **range:** zoom slider while dragging it ([89567d4](https://github.com/ritz078/react-filters/commit/89567d4))
128 | * **range-slider:** completed the basic functionalities of a range slider ([bbf9b1b](https://github.com/ritz078/react-filters/commit/bbf9b1b))
129 | * **slider:** added rails support is value slider ([d6465c9](https://github.com/ritz078/react-filters/commit/d6465c9))
130 | * **slider:** added single control option in slider ([9ffe946](https://github.com/ritz078/react-filters/commit/9ffe946))
131 | * **slider:** added support for onDragStart and onDragEnd methods ([c4941ce](https://github.com/ritz078/react-filters/commit/c4941ce))
132 | * **slider:** added support for vertical slider ([3ae809b](https://github.com/ritz078/react-filters/commit/3ae809b))
133 | * **styleProcessor:** added utility to process style based on different states ([c455c03](https://github.com/ritz078/react-filters/commit/c455c03))
134 | * **switch:** created a container to preview switch ([8f35ec7](https://github.com/ritz078/react-filters/commit/8f35ec7))
135 | * **tag:** added tag feature in autocomplete and refactored it ([2a35097](https://github.com/ritz078/react-filters/commit/2a35097))
136 | * **toggle:** added tag mode to Toggle Component working for radio and checkbox type ([37fc772](https://github.com/ritz078/react-filters/commit/37fc772))
137 |
138 |
139 | ### Performance Improvements
140 |
141 | * **range:** decreased the number of re renders of the elements ([a6369f2](https://github.com/ritz078/react-filters/commit/a6369f2))
142 | * **range:** improved the responsiveness of Range Slider ([1857274](https://github.com/ritz078/react-filters/commit/1857274))
143 | * **range:** optimized width calculations of slider and track ([78b8598](https://github.com/ritz078/react-filters/commit/78b8598))
144 | * **range:** removed forced layout update ([c476203](https://github.com/ritz078/react-filters/commit/c476203))
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------