├── .editorconfig
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── example
├── index.html
├── index.tsx
└── style.css
├── jest.config.js
├── jest.setup.js
├── lib
└── index.js
├── package-lock.json
├── package.json
├── src
├── __tests__
│ ├── __snapshots__
│ │ └── circle-slider.test.tsx.snap
│ └── circle-slider.test.tsx
├── circle-slider
│ ├── helpers
│ │ ├── circle-slider-helper
│ │ │ └── index.ts
│ │ ├── mouse-helper
│ │ │ └── index.ts
│ │ └── path-generator
│ │ │ └── index.ts
│ └── index.tsx
└── index.ts
├── tsconfig.jest.json
├── tsconfig.json
├── tslint.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset=utf-8
3 | end_of_line=crlf
4 | insert_final_newline=true
5 | indent_style=space
6 | indent_size=4
7 |
8 | [{.babelrc,.stylelintrc,.eslintrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
9 | indent_style=space
10 | indent_size=2
11 |
12 | [tslint.json]
13 | indent_style=space
14 | indent_size=2
15 |
16 | [{.analysis_options,*.yml,*.yaml}]
17 | indent_style=space
18 | indent_size=2
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General files
2 | *~
3 | .DS_Store
4 |
5 | # Build files
6 | /styleguide/
7 | /dist/
8 | /build/
9 | .cache
10 |
11 | # Development stuff
12 | node_modules
13 | *.sublime-workspace
14 | *.pyc
15 | .vagrant
16 | .idea
17 | .vscode
18 | npm-debug.*
19 |
20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
22 |
23 | # User-specific stuff:
24 | .idea/**/workspace.xml
25 | .idea/**/tasks.xml
26 |
27 | # Sensitive or high-churn files:
28 | .idea/**/dataSources/
29 | .idea/**/dataSources.ids
30 | .idea/**/dataSources.xml
31 | .idea/**/dataSources.local.xml
32 | .idea/**/sqlDataSources.xml
33 | .idea/**/dynamic.xml
34 | .idea/**/uiDesigner.xml
35 |
36 | # Gradle:
37 | .idea/**/gradle.xml
38 | .idea/**/libraries
39 |
40 | # Mongo Explorer plugin:
41 | .idea/**/mongoSettings.xml
42 |
43 | ## File-based project format:
44 | *.iws
45 |
46 | ## Plugin-specific files:
47 |
48 | # IntelliJ
49 | /out/
50 |
51 | # mpeltonen/sbt-idea plugin
52 | .idea_modules/
53 |
54 | # JIRA plugin
55 | atlassian-ide-plugin.xml
56 |
57 | # Crashlytics plugin (for Android Studio and IntelliJ)
58 | com_crashlytics_export_strings.xml
59 | crashlytics.properties
60 | crashlytics-build.properties
61 | fabric.properties
62 |
63 | # Logs
64 | logs
65 | *.log
66 | npm-debug.log*
67 |
68 | /coverage/
69 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .cache
4 | example
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "trailingComma": "all",
4 | "printWidth": 80
5 | }
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | script:
5 | - npm run build
6 | deploy:
7 | provider: pages
8 | local-dir: dist
9 | skip_cleanup: true
10 | github_token: $GITHUB_TOKEN
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
4 |
5 | ## 1.6.2
6 |
7 | - Fixed issues to make the slider work in IE
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🕹️ react-circle-slider
2 |
3 | Circle Slider Component for React.js
4 |
5 | 
6 |
7 | ## ⚡ Functionality
8 |
9 | - Simple to use
10 | - No extra dependencies
11 | - Highly customizable
12 | - Defining min and max values
13 | - Defining step size
14 | - Defining gradient color
15 | - Touch support
16 | - Tooltip support
17 | - Style based: no images / SVGs
18 |
19 | ## Examples
20 |
21 | - To check out live examples visit https://dmitrymorozoff.github.io/react-circle-slider/
22 |
23 | ## 🚀 Getting started
24 |
25 | Install `react-circle-slider` using npm.
26 |
27 | ### `npm install --save react-circle-slider`
28 |
29 |
30 | You can also test the components locally by cloning this repo and doing the following steps:
31 |
32 | ## 🔲 NPM-scripts
33 |
34 | Install dependencies from package.json:
35 |
36 | ### `npm install`
37 |
38 | Runs the app in the development mode.
39 | Open [http://localhost:1234](http://localhost:1234) to view it in the browser.
40 |
41 | ### `npm run dev`
42 |
43 | Run linter
44 |
45 | ### `npm run lint`
46 |
47 | Start tests followed by jest
48 |
49 | ### `npm run test`
50 |
51 | ## Usage
52 |
53 | ```jsx
54 | import React from "react";
55 | import ReactDOM from "react-dom";
56 | import { CircleSlider } from "react-circle-slider";
57 |
58 | export class App extends React.Component {
59 | constructor(props) {
60 | super(props);
61 | this.state = { value: 0 };
62 | }
63 |
64 | handleChange = value => {
65 | console.log(`Changed value ${value}`);
66 | this.setState({ value });
67 | };
68 |
69 | handleChangeRange = event => {
70 | this.setState({
71 | value: event.target.valueAsNumber,
72 | });
73 | };
74 |
75 | render() {
76 | const { value } = this.state;
77 | return (
78 |
79 | );
80 | }
81 | }
82 |
83 | ReactDOM.render(, document.getElementById("root"));
84 | ```
85 |
86 | ## 📃 Props
87 |
88 | | Props | Type | Default | Description |
89 | | ----------------- | :------- | --------- | -------------------------------------------------------------- |
90 | | size | Number | 180 | size of the slider in px |
91 | | stepSize | Number | 1 | value to be added or subtracted on each step the slider makes. |
92 | | knobRadius | Number | null | knob radius in px |
93 | | circleWidth | Number | null | width of circle in px |
94 | | progressWidth | Number | null | progress curve width in px |
95 | | min | Number | 0 | the minimum value of the slider |
96 | | max | Number | 100 | the maximum value of the slider |
97 | | value | Number | 0 | value |
98 | | circleColor | String | `#e9eaee` | color of slider |
99 | | progressColor | String | `#007aff` | color of progress curve |
100 | | gradientColorFrom | String | NOOP | start gradient color of progress curve |
101 | | gradientColorTo | String | NOOP | end gradient color progress curve |
102 | | knobColor | String | `#fff` | color of knob |
103 | | disabled | Boolean | false | disabled status |
104 | | shadow | Boolean | true | shadow on knob |
105 | | showTooltip | Boolean | false | tooltip |
106 | | showPercentage | Boolean | false | percentage on tooltip |
107 | | tooltipSize | Number | 32 | size of tooltip |
108 | | tooltipColor | String | `#333` | color of tooltip |
109 | | onChange | Function | NOOP | when slider is moved, `onChange` is triggered. |
110 |
111 | ## 💡 Todo
112 |
113 | - [ ] Keyboard support
114 | - [ ] Mouse scroll support
115 | - [ ] Accessibility
116 |
117 | ## 💻 Contributing
118 |
119 | - For bugs and feature requests, please create an issue
120 | - Lint and test your code
121 | - Pull requests and ⭐ stars are always welcome
122 |
123 | ## License
124 |
125 | MIT
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | react-circle-slider
11 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import SyntaxHighlighter from "react-syntax-highlighter/prism";
4 | import { ghcolors } from "react-syntax-highlighter/styles/prism";
5 | import { CircleSlider } from "../src/circle-slider";
6 |
7 | interface IState {
8 | value: number;
9 | }
10 |
11 | export class App extends React.Component<{}, IState> {
12 | constructor(props: any) {
13 | super(props);
14 | this.state = { value: 50 };
15 | }
16 | public handleChange = (value: any) => {
17 | this.setState({ value });
18 | };
19 |
20 | public handleChangeRange = (event: any) => {
21 | this.setState({
22 | value: event.target.valueAsNumber,
23 | });
24 | };
25 |
26 | public render() {
27 | const { value } = this.state;
28 | const codeString = `npm install --save react-circle-slider`;
29 | return (
30 |
31 |
32 |
react-circle-slider
33 |
34 | Circle Slider Component for React.js
35 |
36 |
37 |
38 |
39 |
44 | {codeString}
45 |
46 |
47 |
Default
48 |
49 |
50 |
54 |
{value}
55 |
56 |
64 |
{" "}
65 |
66 |
67 |
72 | {``}
73 |
74 |
75 |
76 |
Custom progress color
77 |
78 |
79 |
84 |
{value}
85 |
86 |
94 |
95 |
96 |
97 |
98 | {``}
102 |
103 |
104 |
105 |
Custom gradient color
106 |
107 |
108 |
116 |
{value}
117 |
118 |
126 |
127 |
128 |
129 |
130 | {``}
137 |
138 |
139 |
140 |
Colors
141 |
142 |
143 |
155 |
{value}
156 |
157 |
165 |
166 |
167 |
168 |
169 | {``}
180 |
181 |
182 |
183 |
Shadow
184 |
185 |
186 |
196 |
{value}
197 |
198 |
206 |
207 |
208 |
209 |
210 | {``}
219 |
220 |
221 |
222 |
Sizes
223 |
224 |
225 |
234 |
{value}
235 |
236 |
244 |
245 |
246 |
247 |
248 | {``}
256 |
257 |
258 |
259 |
Exact sizes
260 |
261 |
262 |
271 |
{value}
272 |
273 |
281 |
282 |
283 |
284 |
285 | {``}
293 |
294 |
295 |
296 |
297 |
298 | );
299 | }
300 | }
301 |
302 | ReactDOM.render(, document.getElementById("root"));
303 |
--------------------------------------------------------------------------------
/example/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-wrap: wrap;
4 | align-items: center;
5 | justify-content: center;
6 | margin: 0 auto;
7 | width: 900px;
8 | }
9 |
10 | .header {
11 | display: flex;
12 | flex-direction: column;
13 | width: 100%;
14 | align-items: center;
15 | justify-content: center;
16 | height: 200px;
17 | background-color: #017aff;
18 | }
19 |
20 | .header-title {
21 | color: #fff;
22 | font-size: 2.125rem;
23 | letter-spacing: 1rem;
24 | text-transform: uppercase;
25 | text-align: center;
26 | margin-bottom: 1rem;
27 | }
28 |
29 | .header-subtitle {
30 | color: #fff;
31 | font-size: 16px;
32 | text-align: center;
33 | opacity: 0.6;
34 | }
35 |
36 | .wrapper {
37 | width: 100%;
38 | display: flex;
39 | align-items: center;
40 | margin: 2rem 0;
41 | border: 1px solid #e9eaee;
42 | padding: 2rem;
43 | box-sizing: border-box;
44 | box-shadow: 0 70px 63px -60px #eeeeee;
45 | }
46 |
47 | .slider {
48 | width: 40%;
49 | display: flex;
50 | flex-direction: column;
51 | justify-content: center;
52 | align-items: center;
53 | }
54 |
55 | .title {
56 | text-align: center;
57 | font-family: sans-serif;
58 | margin-top: 0.5rem;
59 | font-size: 20px;
60 | }
61 |
62 | .range {
63 | display: flex;
64 | justify-content: center;
65 | margin-top: 1rem;
66 | }
67 |
68 | .code {
69 | width: 100%;
70 | margin: 1rem 0 1.5rem 0;
71 | font-size: 18px;
72 | }
73 |
74 | .code>pre>code {
75 | line-height: 1.5rem !important;
76 | }
77 |
78 | h3 {
79 | font-weight: normal;
80 | font-size: 28px;
81 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const TEST_REGEX = "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$";
2 |
3 | module.exports = {
4 | setupFiles: ["/jest.setup.js"],
5 | snapshotSerializers: ["/node_modules/enzyme-to-json/serializer"],
6 | globals: {
7 | "ts-jest": {
8 | // useBabelrc: true,
9 | tsConfigFile: "tsconfig.jest.json",
10 | },
11 | },
12 | testRegex: TEST_REGEX,
13 | transform: {
14 | "^.+\\.tsx?$": "ts-jest",
15 | },
16 | testPathIgnorePatterns: ["/node_modules/"],
17 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
18 | };
19 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | const Enzyme = require("enzyme");
2 | const Adapter = require("enzyme-adapter-react-16");
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | !function(e,t){for(var r in t)e[r]=t[r]}(exports,function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(1);t.CircleSlider=n.CircleSlider},function(e,t,r){"use strict";var n,o=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0});var i=r(2),u=r(5),l=r(6),a=r(7),s=function(e){function t(t){var r=e.call(this,t)||this;r.updateAngle=function(e){r.circleSliderHelper.updateStepIndexFromAngle(e);var t=r.circleSliderHelper.getCurrentStep();r.setState({angle:e,currentStepValue:t}),r.props.onChange(t)},r.updateSlider=function(){var e=r.mouseHelper.getNewSliderAngle();Math.abs(e-r.state.angle)k.length&&k.push(e)}function T(e,t,r){return null==e?0:function e(t,r,n,o){var l=typeof t;"undefined"!==l&&"boolean"!==l||(t=null);var a=!1;if(null===t)a=!0;else switch(l){case"string":case"number":a=!0;break;case"object":switch(t.$$typeof){case i:case u:a=!0}}if(a)return n(o,t,""===r?"."+F(t,0):r),1;if(a=0,r=""===r?".":r+":",Array.isArray(t))for(var s=0;s",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "@babel/core": "^7.2.2",
30 | "@babel/preset-env": "^7.2.3",
31 | "@types/enzyme": "^3.1.13",
32 | "@types/enzyme-adapter-react-16": "^1.0.3",
33 | "@types/enzyme-to-json": "^1.5.2",
34 | "@types/jest": "^23.3.1",
35 | "@types/react": "^16.4.11",
36 | "@types/react-dom": "^16.0.7",
37 | "@types/react-syntax-highlighter": "0.0.6",
38 | "awesome-typescript-loader": "^5.2.1",
39 | "babel-loader": "^8.0.5",
40 | "enzyme": "^3.5.0",
41 | "enzyme-adapter-react-16": "^1.2.0",
42 | "enzyme-to-json": "^3.3.4",
43 | "jest": "^23.5.0",
44 | "parcel-bundler": "^1.9.7",
45 | "prettier": "^1.14.2",
46 | "react-addons-test-utils": "^15.5.1",
47 | "react-syntax-highlighter": "^9.0.1",
48 | "rimraf": "^2.6.2",
49 | "sinon": "^6.1.5",
50 | "source-map-loader": "^0.2.4",
51 | "ts-jest": "^23.1.4",
52 | "ts-loader": "^4.5.0",
53 | "tslint": "^5.11.0",
54 | "tslint-config-prettier": "^1.14.0",
55 | "tslint-react": "^3.6.0",
56 | "typescript": "^3.0.1",
57 | "uglifyjs-webpack-plugin": "^1.3.0",
58 | "webpack": "^4.28.4",
59 | "webpack-cli": "^3.1.0"
60 | },
61 | "dependencies": {
62 | "react": "^16.7.0",
63 | "react-dom": "^16.0.0"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/circle-slider.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`circle slider should render a default circle slider 1`] = `
4 |
80 | `;
81 |
--------------------------------------------------------------------------------
/src/__tests__/circle-slider.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { CircleSlider } from "../circle-slider";
3 | import { shallow, mount } from "enzyme";
4 |
5 | describe("circle slider", () => {
6 | const props = {
7 | circleColor: "#EDEDED",
8 | size: 100,
9 | value: 0,
10 | progressColor: "#ADA1FB",
11 | knobColor: "#ADA1FB",
12 | circleWidthInit: 9,
13 | progressWidthInit: 7,
14 | knobRadiusInit: 6,
15 | stepSize: 1,
16 | min: 0,
17 | max: 100,
18 | onChange: jest.fn(),
19 | };
20 |
21 | it("circle slider should render a svg", () => {
22 | const circleSlider = shallow();
23 |
24 | expect(circleSlider.find("svg")).toHaveLength(1);
25 | });
26 |
27 | it("circle slider should render with props", () => {
28 | const circleSlider = mount();
29 |
30 | expect(circleSlider.props().circleColor).toEqual("#EDEDED");
31 | expect(circleSlider.props().value).toEqual(0);
32 | expect(circleSlider.props().progressColor).toEqual("#ADA1FB");
33 | expect(circleSlider.props().knobColor).toEqual("#ADA1FB");
34 | expect(circleSlider.props().circleWidthInit).toEqual(9);
35 | expect(circleSlider.props().progressWidthInit).toEqual(7);
36 | expect(circleSlider.props().knobRadiusInit).toEqual(6);
37 | expect(circleSlider.props().stepSize).toEqual(1);
38 | expect(circleSlider.props().min).toEqual(0);
39 | expect(circleSlider.props().max).toEqual(100);
40 | });
41 |
42 | it("circle slider should call onChange", () => {
43 | const circleSlider = shallow();
44 | (circleSlider.instance() as any).updateAngle();
45 |
46 | expect(props.onChange).toHaveBeenCalled;
47 | });
48 |
49 | it("circle slider should call onChange", () => {
50 | const circleSlider = shallow();
51 | (circleSlider.instance() as any).updateAngle();
52 |
53 | expect(props.onChange).toHaveBeenCalled;
54 | });
55 |
56 | it("circle slider should call mouseDown", () => {
57 | const circleSlider = shallow();
58 | const accuracy = 0.00001;
59 | circleSlider.simulate("mousedown", {
60 | preventDefault: () => {},
61 | });
62 |
63 | expect(circleSlider.state("angle")).toEqual(0 - accuracy);
64 | expect(circleSlider.state("currentStepValue")).toEqual(0);
65 | });
66 |
67 | it("circle slider current step value to be equals 5", () => {
68 | const circleSlider = shallow();
69 | const instance = circleSlider.instance() as any;
70 | instance.updateAngle(0.3125);
71 |
72 | expect(circleSlider.state("currentStepValue")).toEqual(5);
73 | });
74 |
75 | it("should render a default circle slider", () => {
76 | const circleSlider = shallow();
77 | expect(circleSlider).toMatchSnapshot();
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/circle-slider/helpers/circle-slider-helper/index.ts:
--------------------------------------------------------------------------------
1 | interface INumber {
2 | EPSILON: any;
3 | }
4 | declare var Number: INumber;
5 |
6 | export class CircleSliderHelper {
7 | private stepsArray: number[];
8 | private stepIndex: number;
9 | private countSteps: number;
10 |
11 | constructor(stepsArray: number[], initialValue: any) {
12 | this.stepsArray = stepsArray;
13 | this.countSteps = this.stepsArray.length - 1;
14 | this.stepIndex = 0;
15 | this.setCurrentStepIndexFromArray(initialValue);
16 | }
17 |
18 | public getEpsilon() {
19 | let epsilon = 1.0;
20 | while (1.0 + 0.5 * epsilon !== 1.0) {
21 | epsilon *= 0.5;
22 | }
23 | return epsilon;
24 | }
25 |
26 | public getAngle(): number {
27 | const accuracy = 0.00001;
28 | const epsilon = Number.EPSILON || this.getEpsilon();
29 | return (
30 | Math.min(
31 | this.getAnglePoint() * this.stepIndex,
32 | 2 * Math.PI - epsilon,
33 | ) - accuracy
34 | );
35 | }
36 |
37 | public getCurrentStep(): number {
38 | return this.stepsArray[this.stepIndex];
39 | }
40 |
41 | public updateStepIndexFromValue(value: number) {
42 | const isSetValue = this.setCurrentStepIndexFromArray(value);
43 | if (isSetValue) {
44 | return;
45 | }
46 | this.stepIndex = this.countSteps;
47 | }
48 |
49 | public updateStepIndexFromAngle(angle: number) {
50 | const stepIndex = Math.round(angle / this.getAnglePoint());
51 | if (stepIndex < this.countSteps) {
52 | this.stepIndex = stepIndex;
53 | return;
54 | }
55 | this.stepIndex = this.countSteps;
56 | }
57 |
58 | public setCurrentStepIndexFromArray = (value: number): boolean => {
59 | for (let i = 0; i < this.countSteps; i++) {
60 | if (value <= this.stepsArray[i]) {
61 | this.stepIndex = i;
62 | return true;
63 | }
64 | }
65 | this.stepIndex = this.countSteps;
66 | return false;
67 | };
68 |
69 | public getAnglePoint(): number {
70 | return (Math.PI * 2) / this.countSteps;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/circle-slider/helpers/mouse-helper/index.ts:
--------------------------------------------------------------------------------
1 | export class MouseHelper {
2 | private container: any;
3 | private center!: number;
4 | private relativeX!: number;
5 | private relativeY!: number;
6 |
7 | constructor(container: any) {
8 | this.container = container;
9 | this.setPosition({ x: 0, y: 0 });
10 | }
11 |
12 | public setPosition(event: any): void {
13 | if (!this.container) {
14 | return;
15 | }
16 | const rectSize = this.container.getBoundingClientRect();
17 | const width = rectSize.width;
18 | this.center = width / 2;
19 | this.relativeX = event.clientX - rectSize.left;
20 | this.relativeY = event.clientY - rectSize.top;
21 | }
22 |
23 | public getNewSliderAngle(): number {
24 | const angleBetweenTwoVectors = Math.atan2(
25 | this.relativeY - this.center,
26 | this.relativeX - this.center,
27 | );
28 | return (angleBetweenTwoVectors + (3 * Math.PI) / 2) % (2 * Math.PI);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/circle-slider/helpers/path-generator/index.ts:
--------------------------------------------------------------------------------
1 | export const pathGenerator = (
2 | center: number,
3 | radius: number,
4 | direction: number,
5 | x: number,
6 | y: number,
7 | ): string => {
8 | const points = [];
9 | points.push("M" + center);
10 | points.push(center + radius);
11 | points.push("A");
12 | points.push(radius);
13 | points.push(radius);
14 | points.push(0);
15 | points.push(direction);
16 | points.push(1);
17 | points.push(x);
18 | points.push(y);
19 | return points.join(" ");
20 | };
21 |
--------------------------------------------------------------------------------
/src/circle-slider/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { CircleSliderHelper } from "./helpers/circle-slider-helper";
3 | import { MouseHelper } from "./helpers/mouse-helper";
4 | import { pathGenerator } from "./helpers/path-generator";
5 |
6 | interface IProps {
7 | size?: number;
8 | circleWidth?: number;
9 | progressWidth?: number;
10 | knobRadius?: number;
11 | value?: number;
12 | stepSize?: number;
13 | min?: number;
14 | max?: number;
15 | circleColor?: string;
16 | progressColor?: string;
17 | gradientColorFrom?: string;
18 | gradientColorTo?: string;
19 | knobColor?: string;
20 | onChange: ((value?: number) => void);
21 | disabled?: boolean;
22 | shadow?: boolean;
23 | showTooltip?: boolean;
24 | showPercentage?: boolean;
25 | tooltipSize?: number;
26 | tooltipColor?: string;
27 | }
28 |
29 | interface IPoint {
30 | x: number;
31 | y: number;
32 | }
33 |
34 | interface IState {
35 | angle: number;
36 | currentStepValue: number;
37 | isMouseMove: boolean;
38 | }
39 |
40 | export class CircleSlider extends React.Component {
41 | public static defaultProps: Partial = {
42 | circleColor: "#e9eaee",
43 | size: 180,
44 | value: 0,
45 | progressColor: "#007aff",
46 | knobColor: "#fff",
47 | circleWidth: 5,
48 | progressWidth: 20,
49 | knobRadius: 20,
50 | stepSize: 1,
51 | min: 0,
52 | max: 100,
53 | disabled: false,
54 | shadow: true,
55 | showTooltip: false,
56 | showPercentage: false,
57 | tooltipSize: 32,
58 | tooltipColor: "#333",
59 | onChange: () => ({}),
60 | };
61 | private maxLineWidth: number;
62 | private radius: number;
63 | private countSteps: number;
64 | private stepsArray: number[];
65 | private circleSliderHelper: CircleSliderHelper;
66 | private mouseHelper!: MouseHelper;
67 | private svg: any;
68 |
69 | constructor(props: IProps) {
70 | super(props);
71 | this.state = {
72 | angle: 0,
73 | currentStepValue: 0,
74 | isMouseMove: false,
75 | };
76 |
77 | const {
78 | min,
79 | max,
80 | stepSize,
81 | value,
82 | circleWidth,
83 | progressWidth,
84 | knobRadius,
85 | } = this.props;
86 |
87 | this.maxLineWidth = Math.max(circleWidth!, progressWidth!);
88 | this.radius =
89 | this.getCenter() - Math.max(this.maxLineWidth, knobRadius! * 2) / 2;
90 | this.countSteps = 1 + (max! - min!) / stepSize!;
91 | this.stepsArray = this.getStepsArray(min!, stepSize!);
92 |
93 | this.circleSliderHelper = new CircleSliderHelper(
94 | this.stepsArray,
95 | value,
96 | );
97 | }
98 |
99 | public componentDidMount() {
100 | this.mouseHelper = new MouseHelper(this.svg);
101 | this.setState({
102 | angle: this.circleSliderHelper.getAngle(),
103 | currentStepValue: this.circleSliderHelper.getCurrentStep(),
104 | });
105 | }
106 |
107 | public componentWillReceiveProps(nextProps: any) {
108 | if (this.props.value !== nextProps.value && !this.state.isMouseMove) {
109 | this.updateSliderFromProps(nextProps.value);
110 | }
111 | }
112 |
113 | public updateAngle = (angle: number): void => {
114 | this.circleSliderHelper.updateStepIndexFromAngle(angle);
115 | const currentStep = this.circleSliderHelper.getCurrentStep();
116 | this.setState({
117 | angle,
118 | currentStepValue: currentStep,
119 | });
120 | this.props.onChange(currentStep);
121 | };
122 |
123 | public updateSlider = (): void => {
124 | const angle = this.mouseHelper.getNewSliderAngle();
125 | if (Math.abs(angle - this.state.angle) < Math.PI) {
126 | this.updateAngle(angle);
127 | }
128 | };
129 |
130 | public updateSliderFromProps = (valueFromProps: number): void => {
131 | const { stepSize } = this.props;
132 | const newValue = Math.round(valueFromProps / stepSize!) * stepSize!;
133 | this.circleSliderHelper.updateStepIndexFromValue(newValue);
134 | this.setState({
135 | angle: this.circleSliderHelper.getAngle(),
136 | currentStepValue: newValue,
137 | });
138 | };
139 |
140 | public getCenter = (): number => {
141 | return this.props.size! / 2;
142 | };
143 |
144 | public getAngle = (): number => {
145 | return this.state.angle + Math.PI / 2;
146 | };
147 |
148 | public getPointPosition = (): IPoint => {
149 | const center = this.getCenter();
150 | const angle = this.getAngle();
151 | return {
152 | x: center + this.radius * Math.cos(angle),
153 | y: center + this.radius * Math.sin(angle),
154 | };
155 | };
156 |
157 | public getStepsArray = (min: number, stepSize: number): number[] => {
158 | const stepArray = [];
159 | for (let i = 0; i < this.countSteps; i++) {
160 | stepArray.push(min + i * stepSize);
161 | }
162 | return stepArray;
163 | };
164 |
165 | public getPath = (): string => {
166 | const center = this.getCenter();
167 | const direction = this.getAngle() < 1.5 * Math.PI ? 0 : 1;
168 | const { x, y } = this.getPointPosition();
169 | const path = pathGenerator(center, this.radius, direction, x, y);
170 | return path;
171 | };
172 |
173 | public handleMouseMove = (event: Event): void => {
174 | event.preventDefault();
175 | this.setState({
176 | isMouseMove: true,
177 | });
178 | this.mouseHelper.setPosition(event);
179 | this.updateSlider();
180 | };
181 |
182 | public handleMouseUp = (event: Event): void => {
183 | event.preventDefault();
184 | this.setState({
185 | isMouseMove: false,
186 | });
187 | window.removeEventListener("mousemove", this.handleMouseMove);
188 | window.removeEventListener("mouseup", this.handleMouseUp);
189 | };
190 |
191 | public handleMouseDown = (event: React.MouseEvent): void => {
192 | if (!this.props.disabled) {
193 | event.preventDefault();
194 | window.addEventListener("mousemove", this.handleMouseMove);
195 | window.addEventListener("mouseup", this.handleMouseUp);
196 | }
197 | };
198 | public handleTouchMove: any = (
199 | event: React.TouchEvent,
200 | ): void => {
201 | const targetTouches = event.targetTouches;
202 | const countTouches = targetTouches.length;
203 | const currentTouch: React.Touch = targetTouches.item(countTouches - 1)!;
204 | this.mouseHelper.setPosition(currentTouch);
205 | this.updateSlider();
206 | };
207 |
208 | public handleTouchUp = (): void => {
209 | window.removeEventListener("touchmove", this.handleTouchMove);
210 | window.removeEventListener("touchend", this.handleTouchUp);
211 | };
212 |
213 | public handleTouchStart = (): void => {
214 | if (!this.props.disabled) {
215 | window.addEventListener("touchmove", this.handleTouchMove);
216 | window.addEventListener("touchend", this.handleTouchUp);
217 | }
218 | };
219 |
220 | public render() {
221 | const {
222 | size,
223 | progressColor,
224 | gradientColorFrom,
225 | gradientColorTo,
226 | knobColor,
227 | circleColor,
228 | disabled,
229 | shadow,
230 | circleWidth,
231 | progressWidth,
232 | knobRadius,
233 | showTooltip,
234 | showPercentage,
235 | tooltipSize,
236 | tooltipColor,
237 | } = this.props;
238 | const { currentStepValue } = this.state;
239 | const offset = shadow ? "5px" : "0px";
240 | const { x, y } = this.getPointPosition();
241 | const center = this.getCenter();
242 | const isAllGradientColorsAvailable =
243 | gradientColorFrom && gradientColorTo;
244 | return (
245 |
338 | );
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { CircleSlider } from "./circle-slider";
2 |
3 | export { CircleSlider };
4 |
--------------------------------------------------------------------------------
/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "jsx": "react"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "moduleResolution": "node",
6 | "downlevelIteration": true,
7 | "strict": true,
8 | "strictFunctionTypes": false,
9 | "types": ["node"],
10 | "sourceMap": true,
11 | "allowJs": true,
12 | "allowUnreachableCode": true,
13 | "experimentalDecorators": true,
14 | "emitDecoratorMetadata": true,
15 | "jsx": "react",
16 | "noUnusedLocals": true,
17 | "baseUrl": ".",
18 | "skipLibCheck": true
19 | },
20 | "exclude": [
21 | "example",
22 | "node_modules",
23 | "**/*.spec.ts",
24 | "**/*.spec.tsx",
25 | "**/*.test.ts",
26 | "**/*.test.tsx"
27 | ],
28 | "compileOnSave": false
29 | }
30 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "linterOptions": {
4 | "exclude": [
5 | "node_modules/**",
6 | "**/*.spec.ts",
7 | "**/*.spec.tsx",
8 | "**/*.test.ts",
9 | "**/*.test.tsx"
10 | ]
11 | },
12 | "rules": {
13 | "triple-equals": true,
14 | "no-var-requires": false,
15 | "space-before-function-paren": [
16 | true,
17 | {
18 | "anonymous": "never",
19 | "named": "never",
20 | "asyncArrow": "always",
21 | "method": "never",
22 | "constructor": "never"
23 | }
24 | ],
25 | "object-literal-sort-keys": false,
26 | "no-debugger": false,
27 | "member-ordering": [
28 | true,
29 | {
30 | "order": [
31 | "public-static-field",
32 | "protected-static-field",
33 | "private-static-field",
34 | "public-instance-field",
35 | "protected-instance-field",
36 | "private-instance-field",
37 | "public-constructor",
38 | "protected-constructor",
39 | "private-constructor",
40 | "public-static-method",
41 | "protected-static-method",
42 | "private-static-method",
43 | "public-instance-method",
44 | "protected-instance-method",
45 | "private-instance-method"
46 | ]
47 | }
48 | ],
49 | "align": [true, "statements"],
50 | "jsx-no-multiline-js": false,
51 | "no-default-export": true
52 | },
53 | "jsRules": {
54 | "triple-equals": true,
55 | "no-var-requires": false,
56 | "space-before-function-paren": [
57 | true,
58 | {
59 | "anonymous": "never",
60 | "named": "never",
61 | "asyncArrow": "always",
62 | "method": "never",
63 | "constructor": "never"
64 | }
65 | ],
66 | "object-literal-sort-keys": false,
67 | "no-debugger": false,
68 | "no-console": [false],
69 | "eofline": false,
70 | "jsx-no-multiline-js": false,
71 | "no-default-export": true
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const webpack = require("webpack");
3 |
4 | module.exports = {
5 | entry: path.resolve(__dirname, "src/index.ts"),
6 | output: {
7 | path: path.resolve(__dirname, "./lib"),
8 | filename: "index.js",
9 | library: "",
10 | libraryTarget: "commonjs",
11 | },
12 | resolve: {
13 | extensions: [".js", ".jsx", ".json", ".ts", ".tsx"],
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.(ts|tsx)$/,
19 | loader: ["babel-loader", "ts-loader"],
20 | },
21 | ],
22 | },
23 | };
24 |
--------------------------------------------------------------------------------