├── .babelrc
├── .bithoundrc
├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── demo
├── App.jsx
├── Demo.jsx
├── images
│ └── bgnoise_lg.png
├── index.js
└── main.css
├── karma.conf.js
├── lib
├── index_template.ejs
├── post_install.js
└── render.jsx
├── package.json
├── screenshot.png
├── src
└── index.js
├── style.css
├── tests
└── boilerplate_test.js
└── webpack.config.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react"
5 | ],
6 | "env": {
7 | "start": {
8 | "presets": [
9 | "react-hmre"
10 | ]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.bithoundrc:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [
3 | "dist/*.js"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "jasmine": true
7 | },
8 | "plugins": [
9 | "react"
10 | ],
11 | "globals": {
12 | "jest": false
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 6,
16 | "ecmaFeatures": {
17 | "jsx": true
18 | },
19 | "sourceType": "module"
20 | },
21 | "rules": {
22 | "no-underscore-dangle": 0,
23 | "no-use-before-define": 0,
24 | "quotes": [2, "single"],
25 | "comma-dangle": 1,
26 | "jsx-quotes": 1,
27 | "react/jsx-boolean-value": 1,
28 | "react/jsx-no-undef": 1,
29 | "react/jsx-sort-props": 1,
30 | "react/jsx-uses-react": 1,
31 | "react/jsx-uses-vars": 1,
32 | "react/no-did-mount-set-state": 1,
33 | "react/no-did-update-set-state": 1,
34 | "react/no-multi-comp": 1,
35 | "react/no-unknown-property": 1,
36 | "react/prop-types": 1,
37 | "react/react-in-jsx-scope": 1,
38 | "react/self-closing-comp": 1,
39 | "react/wrap-multilines": 1
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | gh-pages/
3 | dist-modules/
4 | node_modules/
5 | dist/
6 | .idea/
7 | npm-debug.log
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo/
2 | dist/
3 | tests/
4 | src/
5 | .*
6 | karma.conf.js
7 | webpack.config.babel.js
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | - "4.0"
5 | - "4.1"
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Juho Vepsalainen
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # react-gradient-color-picker
3 |
4 | This is a simple gradient color picker integrated with react.
5 | The reason I decide to develop it since there's no usable gradient color picker on npm so far (2015/12/30). Please join me to make it better and more useful.
6 | Please checkout the example at [here](https://www.npmjs.com/package/react-gradient-color-picker).
7 |
8 | 
9 |
10 | ## Development
11 |
12 | Please checkout the code at [here](https://github.com/javidhsueh/react-gradient-color-picker).
13 | * Linting - **npm run lint** - Runs ESLint.
14 | * Testing - **npm test** and **npm run tdd** - Runs Karma/Mocha/Chai/Phantom. Code coverage report is generated through istanbul/isparta to `build/`.
15 | * Developing - **npm start** - Runs the development server at *localhost:8080* and use Hot Module Replacement. You can override the default host and port through env (`HOST`, `PORT`).
16 |
17 | ## Installation
18 | ```sh
19 | npm install --save react-gradient-color-picker
20 | ```
21 |
22 | ## Properties
23 | ### stops {array} default:
24 | ```js
25 | [
26 | {offset: 0.0, color: '#f00', opacity: 1.0},
27 | {offset: 0.5, color: '#fff', opacity: 1.0},
28 | {offset: 1.0, color: '#0f0', opacity: 1.0}
29 | ]
30 | ```
31 |
32 | The color stops of the color map.
33 |
34 | ### onChange {func}
35 |
36 | Callback called on every value change.
37 | The return value is a d3 linear color scale. Input value range is between 0 to 1.
38 | It only triggers when the stop color changes or end of dragging the handlers.
39 |
40 | ### width {number}
41 | The width of the component.
42 |
43 | ## API
44 | ### getColorStops
45 | return an array of color stops
46 |
47 | ### getColorMap
48 | return a D3 color scale function.
49 |
50 |
51 | ## Highlighting Demo
52 |
53 | ```js
54 | render() {
55 | var style = {
56 | width: '300px'
57 | };
58 | var stops = [
59 | {offset: 0.0, color: '#f00', opacity: 1.0},
60 | {offset: 0.5, color: '#fff', opacity: 1.0},
61 | {offset: 1.0, color: '#0f0', opacity: 1.0}
62 | ];
63 | var onChangeCallback = function onChangeCallback(colorStops, colorMap) {
64 | // colorStops: an array of color stops
65 | // colorMap: a d3 linear scale function
66 | // how to get the mapped color:
67 | // var mappedColor = colorMap(0.8);
68 | }
69 | return (
70 |
71 |
72 |
73 | );
74 | }
75 | ```
76 |
77 | ## License
78 |
79 | *react-gradient-color-picker* is available under MIT. See LICENSE for more details.
80 |
81 |
--------------------------------------------------------------------------------
/demo/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Fork from 'react-ghfork';
3 | import pkgInfo from '../package.json';
4 | import Demo from './Demo.jsx';
5 |
6 | export default class App extends React.Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/demo/Demo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactGradientColorPicker from '../src/index'
3 |
4 | export default class Demo extends React.Component {
5 |
6 | render() {
7 | var style = {
8 | width: '800px',
9 | height: '200px'
10 | };
11 | var stops = [
12 | {offset: 0.0, color: '#f00', opacity: 1.0},
13 | {offset: 0.5, color: '#0f0', opacity: 0.5},
14 | {offset: 1.0, color: '#00f', opacity: 0.1}
15 | ];
16 | /* eslint-disable no-unused-vars */
17 | var onChangeCallback = function onChangeCallback(colorStops, colorMap) {
18 | // colorStops: an array of color stops
19 | // colorMap: a d3 linear scale function
20 | // how to get the mapped color:
21 | // var mappedColor = colorMap(0.8);
22 | }
23 | /* eslint-enable no-unused-vars */
24 | return (
25 |
26 |
27 |
HSL
28 |
33 |
HCL
34 |
39 |
40 |
41 |
42 |
Lab
43 |
48 |
RGB
49 |
54 |
55 |
56 | );
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/demo/images/bgnoise_lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/javidhsueh/react-gradient-color-picker/2fc8c695b8e74dbbc1328f0729963572df875887/demo/images/bgnoise_lg.png
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App.jsx';
4 |
5 | const app = document.getElementsByClassName('demonstration')[0];
6 |
7 | ReactDOM.render(, app);
8 |
9 |
--------------------------------------------------------------------------------
/demo/main.css:
--------------------------------------------------------------------------------
1 | /* TODO: insert main styles of your demo here */
2 |
3 | body {
4 | background: #fefefe;
5 |
6 | font-family: Sans-Serif;
7 |
8 | line-height: 1.5;
9 | }
10 |
11 | form label {
12 | display: block;
13 | }
14 |
15 | header {
16 | position: fixed;
17 | padding: 0.5em;
18 | top: 0;
19 |
20 | text-align: center;
21 |
22 | color: #333;
23 | border: 0.1em solid #bbb;
24 |
25 | background-image: url(./images/bgnoise_lg.png);
26 | }
27 |
28 | header h1 {
29 | margin: 0;
30 | }
31 |
32 | header .description {
33 | color: #888;
34 | }
35 |
36 | .highlight {
37 | background: rgb(255, 255, 175);
38 | }
39 |
40 | .sticky {
41 | position: fixed;
42 | top: 0;
43 | left: 0;
44 | right: 0;
45 | }
46 |
47 | .github-fork-ribbon {
48 | position: fixed;
49 |
50 | letter-spacing: 0.01em;
51 | }
52 |
53 | article {
54 | display: block !important;
55 |
56 | margin-top: 6em;
57 | margin-left: auto;
58 | margin-right: auto;
59 |
60 | max-width: 768px;
61 | }
62 |
63 | article section {
64 | margin-bottom: 2em;
65 | }
66 |
67 | article h2 {
68 | margin-top: 2em;
69 | }
70 |
71 | pre {
72 | background: #fafefe;
73 | padding: 0.5em;
74 | }
75 |
76 | article .description {
77 | margin: 1em;
78 | padding: 1em;
79 |
80 | max-width: 60em;
81 |
82 | background: #fafafa;
83 |
84 | border: 0.1em solid #eee;
85 | }
86 |
87 | article .description h2 {
88 | margin: 0;
89 | }
90 | .left {
91 | float: left;
92 | }
93 | .right {
94 | float: right;
95 | }
96 |
97 | .halfWidth {
98 | width: 40%;
99 | margin: 30px;
100 | }
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Reference: http://karma-runner.github.io/0.13/config/configuration-file.html
2 | require('babel-register');
3 |
4 | module.exports = function karmaConfig (config) {
5 | config.set({
6 | frameworks: [
7 | // Reference: https://github.com/karma-runner/karma-mocha
8 | // Set framework to mocha
9 | 'mocha',
10 |
11 | // Reference: http://chaijs.com/api/bdd/
12 | // Use chai assertions
13 | 'chai'
14 | ],
15 |
16 | reporters: [
17 | // Reference: https://github.com/mlex/karma-spec-reporter
18 | // Set reporter to print detailed results to console
19 | 'spec',
20 |
21 | // Reference: https://github.com/karma-runner/karma-coverage
22 | // Output code coverage files
23 | 'coverage'
24 | ],
25 |
26 | files: [
27 | // Reference: https://www.npmjs.com/package/phantomjs-polyfill
28 | // Needed because React.js requires bind and phantomjs does not support it
29 | 'node_modules/phantomjs-polyfill/bind-polyfill.js',
30 |
31 | // Grab all files in the tests directory that contain _test.
32 | 'tests/**/*_test.*'
33 | ],
34 |
35 | preprocessors: {
36 | // Reference: http://webpack.github.io/docs/testing.html
37 | // Reference: https://github.com/webpack/karma-webpack
38 | // Convert files with webpack and load sourcemaps
39 | 'tests/**/*_test.*': ['webpack', 'sourcemap']
40 | },
41 |
42 | browsers: [
43 | // Run tests using PhantomJS
44 | 'PhantomJS'
45 | ],
46 |
47 | singleRun: true,
48 |
49 | // Configure code coverage reporter
50 | coverageReporter: {
51 | dir: 'build/coverage/',
52 | type: 'html'
53 | },
54 |
55 | // Test webpack config
56 | webpack: require('./webpack.config.babel'),
57 |
58 | // Hide webpack build information from output
59 | webpackMiddleware: {
60 | noInfo: true
61 | }
62 | });
63 | };
64 |
--------------------------------------------------------------------------------
/lib/index_template.ejs:
--------------------------------------------------------------------------------
1 |
2 | manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>>
3 |
4 |
5 | <%= htmlWebpackPlugin.options.title || 'Webpack App'%>
6 |
7 | <% if (htmlWebpackPlugin.files.favicon) { %>
8 |
9 | <% } %>
10 |
11 | <% if (htmlWebpackPlugin.options.mobile) { %>
12 |
13 | <% } %>
14 |
15 | <% for (var css in htmlWebpackPlugin.files.css) { %>
16 |
17 | <% } %>
18 |
19 | <% if (htmlWebpackPlugin.options.baseHref) { %>
20 |
21 | <% } %>
22 |
23 |
24 |
25 |
34 |
35 |
36 | Demonstration
37 |
38 | <%= htmlWebpackPlugin.options.demonstration %>
39 |
40 |
41 | Documentation
42 |
43 | <%= htmlWebpackPlugin.options.documentation %>
44 |
45 |
46 |
47 |
48 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
49 |
50 | <% } %>
51 |
52 |
53 |
--------------------------------------------------------------------------------
/lib/post_install.js:
--------------------------------------------------------------------------------
1 | // adapted based on rackt/history (MIT)
2 | // Node 0.10+
3 | var execSync = require('child_process').execSync;
4 | var stat = require('fs').stat;
5 |
6 | // Node 0.10 check
7 | // if (!execSync) {
8 | // execSync = require('sync-exec');
9 | // }
10 |
11 | function exec(command) {
12 | execSync(command, {
13 | stdio: [0, 1, 2]
14 | });
15 | }
16 |
17 | stat('dist-modules', function(error, stat) {
18 | // Skip building on Travis
19 | if (process.env.TRAVIS) {
20 | return;
21 | }
22 |
23 | if (error || !stat.isDirectory()) {
24 | exec('npm i babel-cli babel-preset-es2015 babel-preset-react');
25 | exec('npm run dist:modules');
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/lib/render.jsx:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 |
4 | import React from 'react';
5 | import ReactDOM from 'react-dom/server';
6 | import remark from 'remark';
7 | import reactRenderer from 'remark-react';
8 |
9 | export default function (rootPath, context, demoTemplate) {
10 | demoTemplate = demoTemplate || '';
11 |
12 | var readme = fs.readFileSync(
13 | path.join(rootPath, 'README.md'), 'utf8'
14 | );
15 |
16 | return {
17 | name: context.name,
18 | description: context.description,
19 | demonstration: demoTemplate,
20 | documentation: ReactDOM.renderToStaticMarkup(
21 |
22 | {remark().use(reactRenderer).process(readme)}
23 |
24 | )
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gradient-color-picker",
3 | "description": "react gradient color picker",
4 | "author": "Javid Hsueh",
5 | "user": "javidhsueh",
6 | "version": "0.1.2",
7 | "scripts": {
8 | "start": "webpack-dev-server",
9 | "test": "karma start",
10 | "test:tdd": "karma start --auto-watch --no-single-run",
11 | "test:lint": "eslint . --ext .js --ext .jsx --ignore-path .gitignore --ignore-pattern dist",
12 | "gh-pages": "webpack",
13 | "gh-pages:deploy": "gh-pages -d gh-pages",
14 | "gh-pages:stats": "webpack --profile --json > stats.json",
15 | "dist": "webpack",
16 | "dist:min": "webpack",
17 | "dist:modules": "babel ./src --out-dir ./dist-modules",
18 | "pretest": "npm run test:lint",
19 | "preversion": "npm run test && npm run dist && npm run dist:min && git commit --allow-empty -am \"Update dist\"",
20 | "prepublish": "npm run dist:modules",
21 | "postpublish": "npm run gh-pages && npm run gh-pages:deploy",
22 | "postinstall": "node lib/post_install.js"
23 | },
24 | "main": "dist-modules",
25 | "dependencies": {
26 | "d3": "^3.5.12",
27 | "lodash": "^3.10.1",
28 | "react": "^0.14.7",
29 | "react-colors-picker": "^2.3.0",
30 | "react-dom": "^0.14.7"
31 | },
32 | "devDependencies": {
33 | "babel-cli": "^6.5.1",
34 | "babel-core": "^6.5.2",
35 | "babel-loader": "^6.2.3",
36 | "babel-preset-es2015": "^6.5.0",
37 | "babel-preset-react": "^6.5.0",
38 | "babel-preset-react-hmre": "^1.1.0",
39 | "babel-register": "^6.5.2",
40 | "chai": "^3.5.0",
41 | "clean-webpack-plugin": "^0.1.8",
42 | "css-loader": "^0.23.1",
43 | "eslint": "^2.2.0",
44 | "eslint-loader": "^1.3.0",
45 | "eslint-plugin-react": "^4.0.0",
46 | "extract-text-webpack-plugin": "^1.0.1",
47 | "file-loader": "^0.8.5",
48 | "gh-pages": "^0.10.0",
49 | "git-prepush-hook": "^1.0.1",
50 | "highlight.js": "^9.1.0",
51 | "html-webpack-plugin": "^2.9.0",
52 | "isparta-instrumenter-loader": "^1.0.0",
53 | "json-loader": "^0.5.4",
54 | "karma": "^0.13.21",
55 | "karma-chai": "^0.1.0",
56 | "karma-coverage": "^0.5.3",
57 | "karma-mocha": "^0.2.2",
58 | "karma-phantomjs-launcher": "^1.0.0",
59 | "karma-sourcemap-loader": "^0.3.7",
60 | "karma-spec-reporter": "0.0.24",
61 | "karma-webpack": "^1.7.0",
62 | "mocha": "^2.4.5",
63 | "phantomjs-polyfill": "0.0.1",
64 | "phantomjs-prebuilt": "^2.1.4",
65 | "purecss": "^0.6.0",
66 | "react-addons-test-utils": "^0.14.7",
67 | "react-ghfork": "^0.3.2",
68 | "remark": "^4.1.1",
69 | "remark-react": "^2.0.0",
70 | "style-loader": "^0.13.0",
71 | "sync-exec": "^0.6.2",
72 | "system-bell-webpack-plugin": "^1.0.0",
73 | "url-loader": "^0.5.7",
74 | "webpack": "^1.12.14",
75 | "webpack-dev-server": "^1.14.1",
76 | "webpack-merge": "^0.7.3"
77 | },
78 | "repository": {
79 | "type": "git",
80 | "url": "https://github.com/javidhsueh/react-gradient-color-picker.git"
81 | },
82 | "homepage": "https://javidhsueh.github.io/react-gradient-color-picker/",
83 | "bugs": {
84 | "url": "https://github.com/javidhsueh/react-gradient-color-picker/issues"
85 | },
86 | "keywords": [
87 | "react",
88 | "reactjs",
89 | "color",
90 | "picker",
91 | "gradient"
92 | ],
93 | "license": "MIT",
94 | "pre-push": [
95 | "test",
96 | "test:lint"
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/javidhsueh/react-gradient-color-picker/2fc8c695b8e74dbbc1328f0729963572df875887/screenshot.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import _ from 'lodash';
4 | import ColorPicker from 'react-colors-picker';
5 | import d3 from 'd3';
6 | import React from 'react';
7 |
8 | const HandlerWidth = 4;
9 | const ColorPickerWidth = 16;
10 | const Width = 300;
11 | const Height = 15;
12 | const DefaultColorSpace = 'HSL';
13 |
14 | const ColorSpaces = {
15 | 'HSL': d3.interpolateHsl,
16 | 'HCL': d3.interpolateHcl,
17 | 'Lab': d3.interpolateLab,
18 | 'RGB': d3.interpolateRgb
19 | };
20 |
21 | const DefaultStops = [
22 | { offset: 0.0, color: '#00f', opacity: 1.0 },
23 | { offset: 0.5, color: '#0f0', opacity: 0.5 },
24 | { offset: 1.0, color: '#f00', opacity: 1.0 }
25 | ];
26 |
27 | const CompareOffset = function CompareOffset(a, b) {
28 | return a.offset - b.offset;
29 | };
30 | const ColorPickerID = function ColorPickerID(containerID, idx) {
31 | return containerID + '_gc-cp_' + idx;
32 | }
33 |
34 | class ReactGradientColorPicker extends React.Component {
35 |
36 | constructor(props) {
37 | super(props);
38 |
39 | // TODO: how to get auto-expanded width
40 | var rootHeight = Height;
41 | var rootWidth = Width;
42 | if (this.props.width) {
43 | rootWidth = this.props.width;
44 | }
45 |
46 | this.containerID = _.uniqueId('gc-canvas_');
47 | this.svg = null;
48 | var defaultStops = this.props.stops || DefaultStops;
49 | var stops = defaultStops.map((stop, idx) => {
50 | return {
51 | idx: idx,
52 | x: rootWidth * stop.offset,
53 | offset: stop.offset,
54 | color: stop.color,
55 | opacity: stop.opacity
56 | }
57 | });
58 |
59 | var initColorSpace = DefaultColorSpace;
60 | if (this.props.colorSpace) {
61 | if (ColorSpaces.hasOwnProperty(this.props.colorSpace)) {
62 | initColorSpace = this.props.colorSpace
63 | } else {
64 | /* eslint-disable no-console */
65 | console.error('Incorrect props: colorSpace should be one of [HSL,HCL,Lab,RGB]');
66 | /* eslint-enable no-console */
67 | }
68 | }
69 |
70 | // init state
71 | this.state = {
72 | rootWidth: rootWidth,
73 | rootHeight: rootHeight,
74 | colorSpace: initColorSpace,
75 | stops: stops
76 | };
77 | }
78 |
79 | _addHandler(mouseX) {
80 | var offset = 1.0 * mouseX / this.state.rootWidth;
81 | var midColor = this.colorScale(offset);
82 | var newStop = {
83 | idx: this.state.stops.length,
84 | x: mouseX,
85 | offset: offset,
86 | color: midColor,
87 | opacity: 1.0
88 | };
89 | var newStops = this.state.stops.concat([newStop]);
90 | newStops.sort(CompareOffset);
91 | this.setState({ stops: newStops });
92 |
93 | this._notifyChange();
94 | }
95 | _removeHandler(idx) {
96 | const {stops: oldStops} = this.state;
97 | const stopIdx = _.findIndex(this.state.stops, {idx});
98 | if (stopIdx !== -1) {
99 | this.setState({
100 | stops: [
101 | ...oldStops.slice(0, stopIdx),
102 | ...oldStops.slice(stopIdx + 1)
103 | ]
104 | });
105 | }
106 | }
107 |
108 | _dragHandler(d, mouseX, colorPickerID) {
109 | // only update handler position but not state
110 | d.x = mouseX;
111 | d3.select(this).attr('x', mouseX);
112 | d3.select('#' + colorPickerID)
113 | .style('left', (d.x - ColorPickerWidth / 2) + 'px')
114 | .style('top', Height + 'px');
115 | }
116 |
117 | _dragHandlerEnd(d) {
118 | // when the end of drag, update the state once.
119 | var newStops = _.cloneDeep(this.state.stops);
120 | var currentHandler = _.find(newStops, { 'idx': d.idx });
121 | currentHandler.offset = 1.0 * d.x / this.state.rootWidth;
122 | currentHandler.x = d.x;
123 | this.setState({ stops: newStops });
124 |
125 | this._notifyChange();
126 | }
127 |
128 | _notifyChange() {
129 | if (this.props.onChange) {
130 | this.props.onChange(this.state.stops, this.colorScale);
131 | }
132 | }
133 |
134 | componentDidMount() {
135 | // TODO: get the auto-expanded comonent width
136 | var rootWidth = this.refs.root.offsetWidth;
137 | var rootHeight = this.state.rootHeight;
138 | if (this.props.width) {
139 | rootWidth = this.props.width;
140 | }
141 | var newStops = this.state.stops.map((stop) => {
142 | stop.x = stop.offset * rootWidth;
143 | return stop;
144 | });
145 |
146 | // TODO: this is anti-pattern. should fix it soon.
147 | /* eslint-disable react/no-did-mount-set-state */
148 | this.setState({
149 | rootWidth: rootWidth,
150 | stops: newStops
151 | });
152 | /* eslint-enable react/no-did-mount-set-state */
153 |
154 | var self = this;
155 | // init canvas
156 | this.canvas = d3.select('#' + this.containerID)
157 | .append('canvas')
158 | .attr('width', rootWidth)
159 | .attr('height', 1)
160 | .style('width', rootWidth + 'px')
161 | .style('height', rootHeight + 'px');
162 |
163 | this.svg = d3.select('#' + this.containerID)
164 | .append('svg')
165 | .attr('width', rootWidth)
166 | .attr('height', rootHeight);
167 |
168 | this.colorMap = this.svg.append('rect')
169 | .attr('id', 'gc-color-map')
170 | .attr('x', 0)
171 | .attr('y', 0)
172 | .attr('width', rootWidth)
173 | .attr('height', rootHeight)
174 | .attr('fill', 'rgba(0,0,0,0)')
175 | .on('click', function clickColorMap() {
176 | var mouseX = d3.mouse(this)[0];
177 | self._addHandler(mouseX);
178 | });
179 | }
180 |
181 | _refreshCanvas() {
182 | if (this.svg === null) {
183 | return;
184 | }
185 |
186 | var rootWidth = this.state.rootWidth;
187 | var rootHeight = this.state.rootHeight;
188 |
189 | // refresh canvas size
190 | this.canvas
191 | .attr('width', this.state.rootWidth)
192 | .attr('height', 1)
193 | .style('width', this.state.rootWidth + 'px')
194 | .style('height', this.state.rootHeight + 'px');
195 |
196 | this.svg.attr('width', rootWidth)
197 | .attr('height', rootHeight);
198 |
199 | // refresh color scale
200 | var stops = this.state.stops.map(function iterator(s) {
201 | return {
202 | offset: s.offset,
203 | color: s.color,
204 | opacity: s.opacity
205 | };
206 | }).sort(CompareOffset);
207 | var offsets = _.map(stops, 'offset');
208 | var colors = _.map(stops, 'color');
209 | var opacity = _.map(stops, 'opacity');
210 |
211 | this.colorScale = d3.scale.linear()
212 | .domain(offsets)
213 | .range(colors)
214 | .interpolate(ColorSpaces[this.state.colorSpace]);
215 |
216 | this.opacityScale = d3.scale.linear()
217 | .domain(offsets)
218 | .range(opacity);
219 |
220 | var _localColorScale = this.colorScale;
221 | var _localOpacityScale = this.opacityScale;
222 |
223 | this.colorMap
224 | .attr('width', rootWidth)
225 | .attr('height', rootHeight);
226 |
227 | this.canvas.each(function renderCanvas() {
228 | var context = this.getContext('2d'),
229 | image = context.createImageData(rootWidth, 1);
230 | for (var i = 0, j = -1, c, a; i < rootWidth; ++i) {
231 | c = d3.rgb(_localColorScale(i * 1.0 / rootWidth));
232 | a = _localOpacityScale(i * 1.0 / rootWidth);
233 | image.data[++j] = c.r;
234 | image.data[++j] = c.g;
235 | image.data[++j] = c.b;
236 | image.data[++j] = a*255;
237 | }
238 | context.putImageData(image, 0, 0);
239 | })
240 | // refresh gradient
241 | var gradientID = this.containerID + '_gc-gradient';
242 | this.gradient = this.svg.select('#' + gradientID)
243 | .attr('x2', rootWidth)
244 | .selectAll('stop')
245 | .data(this.state.stops);
246 |
247 | // enter stops
248 | this.gradient.enter()
249 | .append('stop')
250 | .attr('offset', function offsetAccessor(d) {
251 | return (d.offset * 100) + '%';
252 | })
253 | .attr('stop-color', function colorAccessor(d) {
254 | return d.color;
255 | });
256 | // update existing stops
257 | this.gradient
258 | .attr('offset', (d) => (d.offset * 100).toString() + '%')
259 | .attr('stop-color', (d) => d.color);
260 |
261 | // remove non-exist stops
262 | this.gradient.exit().remove();
263 |
264 | // refresh handlers
265 | this.handlers = this.svg.selectAll('.gc-handler')
266 | .data(this.state.stops);
267 |
268 | // enter new handlers
269 | var self = this;
270 | var dragCallback = function dragCallback(d) {
271 | var newX = d3.event.x;
272 | if (newX >= 0 && newX <= self.state.rootWidth) {
273 | self._dragHandler.call(this, d, newX, ColorPickerID(self.containerID, d.idx));
274 | }
275 | }
276 | var drag = d3.behavior.drag()
277 | .origin(Object)
278 | .on('drag', dragCallback)
279 | .on('dragend', this._dragHandlerEnd.bind(this));
280 |
281 | this.handlers.enter()
282 | .append('rect')
283 | .attr('class', 'gc-handler')
284 | .attr('x', function xPos(d) {
285 | return d.x - HandlerWidth / 2;
286 | }.bind(this))
287 | .attr('y', '0')
288 | .attr('width', HandlerWidth)
289 | .attr('height', rootHeight)
290 | .call(drag);
291 |
292 | // update existing handlers
293 | this.handlers
294 | .attr('x', function xPos(d) {
295 | return d.x - HandlerWidth / 2;
296 | }.bind(this));
297 |
298 | // remove non-exist handlers
299 | this.handlers.exit().remove();
300 |
301 | // refresh the color pickers
302 | this.state.stops.forEach(function iterator(s) {
303 | d3.select('#' + ColorPickerID(this.containerID, s.idx))
304 | .style('left', (s.x - ColorPickerWidth / 2) + 'px')
305 | .style('top', Height + 'px');
306 | }.bind(this));
307 | }
308 |
309 | render() {
310 | this._refreshCanvas();
311 |
312 | var colorChangeCallback = function colorChangeCallback(color, opacity, idx) {
313 | var newStops = _.cloneDeep(this.state.stops);
314 | var currentHandler = _.find(newStops, { 'idx': idx });
315 | currentHandler.color = color;
316 | currentHandler.opacity = 1.0 * opacity / 100;
317 | this.setState({ stops: newStops });
318 |
319 | // notify change
320 | if (this.props.onChange) {
321 | this.props.onChange(this.state.stops, this.colorScale);
322 | }
323 | }.bind(this);
324 | var colorpickers = this.state.stops.map(function iterator(s) {
325 | let pickerId = ColorPickerID(this.containerID, s.idx);
326 | let removeCallback = () => this._removeHandler(s.idx);
327 | let callback = (c) => colorChangeCallback(c.color, c.alpha, s.idx);
328 | var style = {
329 | left: (s.x - ColorPickerWidth / 2) + 'px',
330 | top: Height + 'px'
331 | }
332 | return (
333 |
334 | < div id = { pickerId }
335 | key = { pickerId } >
336 | < ColorPicker animation = "slide-up"
337 | color = { s.color }
338 | alpha = {s.opacity * 100}
339 | onChange = { callback }
340 | placement = "bottomLeft" / >
341 | < /div>
342 |
x
344 |
345 | );
346 | }.bind(this));
347 | return ( < div className = "gc-container"
348 | ref = "root" > { colorpickers } < div className = "gc-canvas"
349 | id = { this.containerID } > < /div> < /div>
350 | );
351 | }
352 |
353 | // Publid API:
354 | getColorMap() {
355 | return this.colorScale;
356 | }
357 |
358 | getColorStops() {
359 | return this.state.stops;
360 | }
361 | }
362 |
363 | ReactGradientColorPicker.propTypes = {
364 | stops: React.PropTypes.arrayOf(React.PropTypes.object),
365 | colorSpace: React.PropTypes.string,
366 | onChange: React.PropTypes.func,
367 | width: React.PropTypes.number
368 | };
369 |
370 | module.exports = ReactGradientColorPicker;
371 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @import 'node_modules/react-colors-picker/assets/index';
2 |
3 | .gc-container {
4 | width: 100%;
5 | margin: 0 0;
6 | padding: 0 0;
7 | position: relative;
8 | }
9 |
10 | .gc-container .react-colorpicker-trigger {
11 | width: 10px;
12 | height: 10px;
13 | }
14 |
15 | .gc-container .gc-canvas {
16 | width: 100%;
17 | height: 20px;
18 | background-color: white;
19 | }
20 | .gc-container svg {
21 | position: absolute;
22 | top: 0px;
23 | left: 0px;
24 | }
25 |
26 | .gc-container canvas {
27 | position: absolute;
28 | top: 0px;
29 | left: 0px;
30 | }
31 |
32 | .gc-container .gc-handler {
33 | fill: white;
34 | stroke: gray;
35 | fill-opacity: 0.7;
36 | cursor: move;
37 | }
38 |
39 | .gc-container .gc-colorpicker {
40 | position: absolute;
41 | }
42 |
43 | .gc-colorpicker .remove-btn {
44 | position: relative;
45 | position: absolute;
46 | top: 15px;
47 | left: 4px;
48 | cursor: pointer;
49 | opacity: 0;
50 | }
51 | .gc-colorpicker:hover .remove-btn {
52 | opacity: 1;
53 | }
54 | .react-colorpicker {
55 | border: 1px solid #999;
56 | padding: 0px;
57 | border-radius: 2px;
58 | }
59 |
60 | .react-colorpicker-trigger {
61 | border: 0px;
62 | margin-bottom: -2px;
63 | }
--------------------------------------------------------------------------------
/tests/boilerplate_test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import {
3 | // renderIntoDocument,
4 | // findRenderedDOMComponentWithClass,
5 | // findRenderedDOMComponentWithTag,
6 | // Simulate
7 | //} from 'react-addons-test-utils';
8 |
9 | describe('Boilerplate', function() {
10 | it('should do boilerplate things', function() {
11 | // TODO: test something now
12 | expect(true).to.equal(true);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | import webpack from 'webpack';
4 | import HtmlWebpackPlugin from 'html-webpack-plugin';
5 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
6 | import SystemBellPlugin from 'system-bell-webpack-plugin';
7 | import CleanPlugin from 'clean-webpack-plugin';
8 | import merge from 'webpack-merge';
9 | import React from 'react';
10 | import ReactDOM from 'react-dom/server';
11 |
12 | import renderJSX from './lib/render.jsx';
13 | import App from './demo/App.jsx';
14 | import pkg from './package.json';
15 |
16 | const RENDER_UNIVERSAL = true;
17 | const TARGET = process.env.npm_lifecycle_event;
18 | const ROOT_PATH = __dirname;
19 | const config = {
20 | paths: {
21 | dist: path.join(ROOT_PATH, 'dist'),
22 | src: path.join(ROOT_PATH, 'src'),
23 | demo: path.join(ROOT_PATH, 'demo'),
24 | tests: path.join(ROOT_PATH, 'tests')
25 | },
26 | filename: 'boilerplate',
27 | library: 'Boilerplate'
28 | };
29 | const CSS_PATHS = [
30 | config.paths.demo,
31 | path.join(ROOT_PATH, 'style.css'),
32 | path.join(ROOT_PATH, 'node_modules/purecss'),
33 | path.join(ROOT_PATH, 'node_modules/highlight.js/styles/github.css'),
34 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.ie.css'),
35 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.css')
36 | ];
37 | const STYLE_ENTRIES = [
38 | 'purecss',
39 | 'highlight.js/styles/github.css',
40 | 'react-ghfork/gh-fork-ribbon.ie.css',
41 | 'react-ghfork/gh-fork-ribbon.css',
42 | './demo/main.css',
43 | './style.css'
44 | ];
45 |
46 | process.env.BABEL_ENV = TARGET;
47 |
48 | const demoCommon = {
49 | resolve: {
50 | extensions: ['', '.js', '.jsx', '.css', '.png', '.jpg']
51 | },
52 | module: {
53 | preLoaders: [
54 | {
55 | test: /\.jsx?$/,
56 | loaders: ['eslint'],
57 | include: [
58 | config.paths.demo,
59 | config.paths.src
60 | ]
61 | }
62 | ],
63 | loaders: [
64 | {
65 | test: /\.png$/,
66 | loader: 'url?limit=100000&mimetype=image/png',
67 | include: config.paths.demo
68 | },
69 | {
70 | test: /\.jpg$/,
71 | loader: 'file',
72 | include: config.paths.demo
73 | },
74 | {
75 | test: /\.json$/,
76 | loader: 'json',
77 | include: path.join(ROOT_PATH, 'package.json')
78 | }
79 | ]
80 | },
81 | plugins: [
82 | new SystemBellPlugin()
83 | ]
84 | };
85 |
86 | if (TARGET === 'start') {
87 | module.exports = merge(demoCommon, {
88 | devtool: 'eval-source-map',
89 | entry: {
90 | demo: [config.paths.demo].concat(STYLE_ENTRIES)
91 | },
92 | plugins: [
93 | new webpack.DefinePlugin({
94 | 'process.env.NODE_ENV': '"development"'
95 | }),
96 | new HtmlWebpackPlugin(Object.assign({}, {
97 | title: pkg.name + ' - ' + pkg.description,
98 | template: 'lib/index_template.ejs',
99 |
100 | inject: false
101 | }, renderJSX(__dirname, pkg))),
102 | new webpack.HotModuleReplacementPlugin()
103 | ],
104 | module: {
105 | loaders: [
106 | {
107 | test: /\.css$/,
108 | loaders: ['style', 'css'],
109 | include: CSS_PATHS
110 | },
111 | {
112 | test: /\.jsx?$/,
113 | loaders: ['babel?cacheDirectory'],
114 | include: [
115 | config.paths.demo,
116 | config.paths.src
117 | ]
118 | }
119 | ]
120 | },
121 | devServer: {
122 | historyApiFallback: true,
123 | hot: true,
124 | inline: true,
125 | progress: true,
126 | host: process.env.HOST,
127 | port: process.env.PORT,
128 | stats: 'errors-only'
129 | }
130 | });
131 | }
132 |
133 | function NamedModulesPlugin(options) {
134 | this.options = options || {};
135 | }
136 | NamedModulesPlugin.prototype.apply = function(compiler) {
137 | compiler.plugin('compilation', function(compilation) {
138 | compilation.plugin('before-module-ids', function(modules) {
139 | modules.forEach(function(module) {
140 | if(module.id === null && module.libIdent) {
141 | var id = module.libIdent({
142 | context: this.options.context || compiler.options.context
143 | });
144 |
145 | // Skip CSS files since those go through ExtractTextPlugin
146 | if(!id.endsWith('.css')) {
147 | module.id = id;
148 | }
149 | }
150 | }, this);
151 | }.bind(this));
152 | }.bind(this));
153 | };
154 |
155 | if (TARGET === 'gh-pages' || TARGET === 'gh-pages:stats') {
156 | module.exports = merge(demoCommon, {
157 | entry: {
158 | app: config.paths.demo,
159 | vendors: [
160 | 'react'
161 | ],
162 | style: STYLE_ENTRIES
163 | },
164 | output: {
165 | path: './gh-pages',
166 | filename: '[name].[chunkhash].js',
167 | chunkFilename: '[chunkhash].js'
168 | },
169 | plugins: [
170 | new CleanPlugin(['gh-pages'], {
171 | verbose: false
172 | }),
173 | new ExtractTextPlugin('[name].[chunkhash].css'),
174 | new webpack.DefinePlugin({
175 | // This affects the react lib size
176 | 'process.env.NODE_ENV': '"production"'
177 | }),
178 | new HtmlWebpackPlugin(Object.assign({}, {
179 | title: pkg.name + ' - ' + pkg.description,
180 | template: 'lib/index_template.ejs',
181 | inject: false
182 | }, renderJSX(
183 | __dirname, pkg, RENDER_UNIVERSAL ? ReactDOM.renderToString() : '')
184 | )),
185 | new NamedModulesPlugin(),
186 | new webpack.optimize.DedupePlugin(),
187 | new webpack.optimize.UglifyJsPlugin({
188 | compress: {
189 | warnings: false
190 | }
191 | }),
192 | new webpack.optimize.CommonsChunkPlugin({
193 | names: ['vendors', 'manifest']
194 | })
195 | ],
196 | module: {
197 | loaders: [
198 | {
199 | test: /\.css$/,
200 | loader: ExtractTextPlugin.extract('style', 'css'),
201 | include: CSS_PATHS
202 | },
203 | {
204 | test: /\.jsx?$/,
205 | loaders: ['babel'],
206 | include: [
207 | config.paths.demo,
208 | config.paths.src
209 | ]
210 | }
211 | ]
212 | }
213 | });
214 | }
215 |
216 | // !TARGET === prepush hook for test
217 | if (TARGET === 'test' || TARGET === 'test:tdd' || !TARGET) {
218 | module.exports = merge(demoCommon, {
219 | module: {
220 | preLoaders: [
221 | {
222 | test: /\.jsx?$/,
223 | loaders: ['eslint'],
224 | include: [
225 | config.paths.tests
226 | ]
227 | }
228 | ],
229 | loaders: [
230 | {
231 | test: /\.jsx?$/,
232 | loaders: ['babel?cacheDirectory'],
233 | include: [
234 | config.paths.src,
235 | config.paths.tests
236 | ]
237 | }
238 | ]
239 | }
240 | })
241 | }
242 |
243 | const distCommon = {
244 | devtool: 'source-map',
245 | output: {
246 | path: config.paths.dist,
247 | libraryTarget: 'umd',
248 | library: config.library
249 | },
250 | entry: config.paths.src,
251 | externals: {
252 | 'react': {
253 | commonjs: 'react',
254 | commonjs2: 'react',
255 | amd: 'React',
256 | root: 'React'
257 | }
258 | },
259 | module: {
260 | loaders: [
261 | {
262 | test: /\.jsx?$/,
263 | loaders: ['babel'],
264 | include: config.paths.src
265 | }
266 | ]
267 | },
268 | plugins: [
269 | new SystemBellPlugin()
270 | ]
271 | };
272 |
273 | if (TARGET === 'dist') {
274 | module.exports = merge(distCommon, {
275 | output: {
276 | filename: config.filename + '.js'
277 | }
278 | });
279 | }
280 |
281 | if (TARGET === 'dist:min') {
282 | module.exports = merge(distCommon, {
283 | output: {
284 | filename: config.filename + '.min.js'
285 | },
286 | plugins: [
287 | new webpack.optimize.UglifyJsPlugin({
288 | compress: {
289 | warnings: false
290 | }
291 | })
292 | ]
293 | });
294 | }
295 |
--------------------------------------------------------------------------------