├── .eslintrc
├── .gitignore
├── README.md
├── _config.yml
├── assets
└── img
│ └── grapick.jpg
├── dist
├── grapick.min.css
└── grapick.min.js
├── index.html
├── package-lock.json
├── package.json
├── src
├── Grapick.js
├── Handler.js
├── emitter.js
├── index.js
├── styles
│ └── main.scss
└── utils.js
├── test
└── index.js
├── webpack.config.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true
5 | },
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "ecmaFeatures": {
9 | "classes": true,
10 | "experimentalObjectRestSpread": true
11 | }
12 | },
13 | "rules": {
14 | "strict": 0,
15 | "quotes": [0, "single"],
16 | "eol-last": [0],
17 | "no-mixed-requires": [0],
18 | "no-underscore-dangle": [0]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .sass-cache
3 | npm-debug.log
4 | private/
5 | docs/
6 | coverage/
7 | node_modules/
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Grapick](https://artf.github.io/grapick)
2 |
3 |
4 | Easy configurable gradient picker, with no dependencies.
5 |
6 |

7 |
8 | [Demo](https://artf.github.io/grapick)
9 |
10 |
11 |
12 |
13 |
14 | ## Download
15 |
16 | You can download the file from [here](https://cdn.rawgit.com/artf/grapick/master/dist/grapick.min.js) ([CSS](https://cdn.rawgit.com/artf/grapick/master/dist/grapick.min.css)), via `npm i grapick` or directly from the `/dist` folder of this repo
17 |
18 |
19 |
20 |
21 |
22 | ## Usage
23 |
24 | ```html
25 |
26 |
27 |
28 |
29 |
30 |
42 | ```
43 |
44 |
45 |
46 |
47 |
48 | # Configurations
49 |
50 | * `pfx` - Class prefix (string)
51 | * `el` - Element on which the picker will be attached (HTMLElement or query string)
52 | * `colorEl` - Element to use for the custom color picker, eg. ''
53 | * `min` - Minimum handler position, default: 0 (integer)
54 | * `max` - Maximum handler position, default: 100 (integer)
55 | * `direction` - Any supported gradient direction: '90deg' (default), 'top', 'bottom', 'right', '135deg', etc.
56 | * `type` - Gradient type, available options: 'linear' (default) | 'radial' | 'repeating-linear' | 'repeating-radial'
57 | * `height` - Gradient input height, default: '30px'
58 | * `width` - Gradient input width, default: '100%'
59 | * `emptyColor` - Default empty color (when you click on an empty color picker area)
60 | * `onValuePos` - Format handler position value, default (to avoid floats): val => parseInt(val)
61 |
62 |
63 |
64 |
65 |
66 | ## Add custom color picker
67 |
68 | Grapick is color picker independent and uses the browser's native one, by default, just to make it more accessible, but you can easily switch it with one of your choices (recommended as not all browsers support properly `input[type=color]`).
69 |
70 | In the example below we use [spectrum](https://github.com/bgrins/spectrum) color picker just as the proof of concept
71 |
72 | ```html
73 |
74 |
75 |
76 |
77 |
78 |
79 |
106 | ```
107 |
108 |
109 |
110 | ## Events
111 |
112 | Available events
113 |
114 | * `change` - Gradient is changed
115 | * `handler:drag:start` - Started dragging the handler
116 | * `handler:drag` - Dragging the handler
117 | * `handler:drag:end` - Stopped dragging the handler
118 | * `handler:select` - Handler selected
119 | * `handler:deselect` - Handler deselected
120 | * `handler:add` - New handler added
121 | * `handler:remove` - Handler removed
122 | * `handler:color:change` - The color of the handler is changed
123 | * `handler:position:change` - The position of the handler is changed
124 |
125 |
126 |
127 |
128 |
129 | ## Development
130 |
131 | Clone the repository and enter inside the folder
132 |
133 | ```sh
134 | $ git clone https://github.com/artf/grapick.git
135 | $ cd grapick
136 | ```
137 |
138 | Install it
139 |
140 | ```sh
141 | $ npm i
142 | ```
143 |
144 | Start the dev server
145 |
146 | ```sh
147 | $ npm start
148 | ```
149 |
150 |
151 |
152 |
153 |
154 | ## API
155 |
156 | [API Reference here](https://github.com/artf/grapick/wiki)
157 |
158 |
159 |
160 |
161 |
162 | ## Testing
163 |
164 | Run tests
165 |
166 | ```sh
167 | $ npm test
168 | ```
169 |
170 | Run and watch tests
171 |
172 | ```sh
173 | $ npm run test:dev
174 | ```
175 |
176 |
177 |
178 |
179 |
180 | ## License
181 |
182 | MIT
183 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/assets/img/grapick.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artf/grapick/bcd49de389b06eb58c970ae13773577ac69a5962/assets/img/grapick.jpg
--------------------------------------------------------------------------------
/dist/grapick.min.css:
--------------------------------------------------------------------------------
1 | .grp-wrapper{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==")}.grp-preview{position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair}.grp-handler{width:4px;margin-left:-2px;user-select:none;-webkit-user-select:none;-moz-user-select:none;height:100%}.grp-handler-close{color:rgba(0,0,0,0.4);border-radius:100%;box-shadow:0 2px 10px rgba(0,0,0,0.25);background-color:#fff;text-align:center;width:15px;height:15px;margin-left:-5px;line-height:10px;font-size:21px;cursor:pointer}.grp-handler-close-c{position:absolute;top:-17px}.grp-handler-drag{background-color:rgba(0,0,0,0.5);cursor:col-resize;width:100%;height:100%}.grp-handler-selected .grp-handler-drag{background-color:rgba(255,255,255,0.5)}.grp-handler-cp-c{display:none}.grp-handler-selected .grp-handler-cp-c{display:block}.grp-handler-cp-wrap{width:15px;height:15px;margin-left:-8px;border:3px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,0.25);overflow:hidden;border-radius:100%;cursor:pointer}.grp-handler-cp-wrap input[type=color]{opacity:0;cursor:pointer}
2 |
--------------------------------------------------------------------------------
/dist/grapick.min.js:
--------------------------------------------------------------------------------
1 | /*! grapick - 0.1.13 */
2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Grapick=t():e.Grapick=t()}("undefined"!=typeof self?self:this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var r=n[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=1)}([function(e,t,n){"use strict";function i(e,t,n){t=t.split(/\s+/);for(var i=0;i0&&void 0!==arguments[0]?arguments[0]:{};o(this,t);var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));e=Object.assign({},e);var i={pfx:"grp",el:".grp",colorEl:"",min:0,max:100,direction:"90deg",type:"linear",height:"30px",width:"100%",emptyColor:"#000",onValuePos:function(e){return parseInt(e)}};for(var r in i)r in e||(e[r]=i[r]);var l=e.el;if(!((l="string"==typeof l?document.querySelector(l):l)instanceof HTMLElement))throw"Element not found, given "+l;return n.el=l,n.handlers=[],n.options=e,n.on("handler:color:change",function(e,t){return n.change(t)}),n.on("handler:position:change",function(e,t){return n.change(t)}),n.on("handler:remove",function(e){return n.change(1)}),n.on("handler:add",function(e){return n.change(1)}),n.render(),n}return l(t,e),u(t,[{key:"destroy",value:function(){var e=this;this.clear(),this.e={},["el","handlers","options","colorPicker"].forEach(function(t){return e[t]=0}),["previewEl","wrapperEl","sandEl"].forEach(function(t){var n=e[t];n&&n.parentNode&&n.parentNode.removeChild(n),delete e[t]})}},{key:"setColorPicker",value:function(e){this.colorPicker=e}},{key:"getValue",value:function(e,t){var n=this.getColorValue(),i=e||this.getType(),r=["top","left","bottom","right","center"],o=t||this.getDirection();return["linear","repeating-linear"].indexOf(i)>=0&&r.indexOf(o)>=0&&(o="center"===o?"to right":"to "+o),["radial","repeating-radial"].indexOf(i)>=0&&r.indexOf(o)>=0&&(o="circle at "+o),n?i+"-gradient("+o+", "+n+")":""}},{key:"getSafeValue",value:function(e,t){var n=this.previewEl,i=this.getValue(e,t);if(!this.sandEl&&(this.sandEl=document.createElement("div")),!n||!i)return"";for(var o=this.sandEl.style,a=[i].concat(r(this.getPrefixedValues(e,t))),l=void 0,u=0;u0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=this.type,r=this.direction,o=t.indexOf("(")+1,a=t.lastIndexOf(")"),l=t.substring(o,a),u=l.split(/,(?![^(]*\)) /);if(this.clear(n),!l)return void this.updatePreview();u.length>2&&(r=u.shift());var s=void 0;["repeating-linear","repeating-radial","linear","radial"].forEach(function(e){t.indexOf(p(e))>-1&&!s&&(s=1,i=e)}),this.setDirection(r,n),this.setType(i,n),u.forEach(function(t){var i=t.split(" "),r=parseFloat(i.pop()),o=i.join("");e.addHandler(r,o,0,n)}),this.updatePreview()}},{key:"getColorValue",value:function(){var e=this.handlers;return e.sort(v),e=1==e.length?[e[0],e[0]]:e,e.map(function(e){return e.getValue()}).join(", ")}},{key:"getPrefixedValues",value:function(e,t){var n=this.getValue(e,t);return["-moz-","-webkit-","-o-","-ms-"].map(function(e){return""+e+n})}},{key:"change",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.updatePreview(),!t.silent&&this.emit("change",e)}},{key:"setDirection",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.options.direction=e;var n=t.complete,i=void 0===n?1:n;this.change(i,t)}},{key:"getDirection",value:function(){return this.options.direction}},{key:"setType",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.options.type=e;var n=t.complete,i=void 0===n?1:n;this.change(i,t)}},{key:"getType",value:function(){return this.options.type}},{key:"addHandler",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},r=new f.default(this,e,t,n,i);return!i.silent&&this.emit("handler:add",r),r}},{key:"getHandler",value:function(e){return this.handlers[e]}},{key:"getHandlers",value:function(){return this.handlers}},{key:"clear",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=this.handlers,n=t.length-1;n>=0;n--)t[n].remove(e)}},{key:"getSelected",value:function(){for(var e=this.getHandlers(),t=0;to||sa.h||u<0)){var c=document.createElement("canvas"),h=c.getContext("2d");c.width=a.w,c.height=a.h;var f=h.createLinearGradient(0,0,a.w,a.h);e.getHandlers().forEach(function(e){return f.addColorStop(e.position/100,e.color)}),h.fillStyle=f,h.fillRect(0,0,c.width,c.height),c.style.background="black";var d=c.getContext("2d").getImageData(l,u,1,1).data,v="rgba("+d[0]+", "+d[1]+", "+d[2]+", "+d[3]+")",p="rgba(0, 0, 0, 0)"==v?i.emptyColor:v;e.addHandler(s,p)}})}},{key:"render",value:function(){var e=this.options,t=this.el,n=e.pfx,i=e.height,r=e.width;if(t){var o=n+"-wrapper",a=n+"-preview";t.innerHTML='\n \n ';var l=t.querySelector("."+o),u=t.querySelector("."+a),s=l.style;s.position="relative",this.wrapperEl=l,this.previewEl=u,i&&(s.height=i),r&&(s.width=r),this.initEvents(),this.updatePreview()}}}]),t}(c.default);t.default=g},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:0,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"black",o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1,a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};i(this,e),t.getHandlers().push(this),this.gp=t,this.position=n,this.color=r,this.selected=0,this.render(),o&&this.select(a)}return r(e,[{key:"toJSON",value:function(){return{position:this.position,selected:this.selected,color:this.color}}},{key:"setColor",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;this.color=e,this.emit("handler:color:change",this,t)}},{key:"setPosition",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=this.getEl();this.position=e,n&&(n.style.left=e+"%"),this.emit("handler:position:change",this,t)}},{key:"getColor",value:function(){return this.color}},{key:"getPosition",value:function(){var e=this.position,t=this.gp,n=t.options.onValuePos;return(0,o.isFunction)(n)?n(e):e}},{key:"isSelected",value:function(){return!!this.selected}},{key:"getValue",value:function(){return this.getColor()+" "+this.getPosition()+"%"}},{key:"select",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=this.getEl(),n=this.gp.getHandlers();!e.keepSelect&&n.forEach(function(e){return e.deselect()}),this.selected=1;var i=this.getSelectedCls();t&&(t.className+=" "+i),this.emit("handler:select",this)}},{key:"deselect",value:function(){var e=this.getEl();this.selected=0;var t=this.getSelectedCls();e&&(e.className=e.className.replace(t,"").trim()),this.emit("handler:deselect",this)}},{key:"getSelectedCls",value:function(){return this.gp.options.pfx+"-handler-selected"}},{key:"remove",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=this.cpFn,i=this.getEl(),r=this.gp.getHandlers(),a=r.splice(r.indexOf(this),1)[0];return i&&i.parentNode.removeChild(i),!t.silent&&this.emit("handler:remove",a),(0,o.isFunction)(n)&&n(this),["el","gp"].forEach(function(t){return e[t]=0}),a}},{key:"getEl",value:function(){return this.el}},{key:"initEvents",value:function(){var e=this,t=this.getEl(),n=this.gp.previewEl,i=this.gp.options,r=i.min,a=i.max,l=t.querySelector("[data-toggle=handler-close]"),u=t.querySelector("[data-toggle=handler-color-c]"),s=t.querySelector("[data-toggle=handler-color-wrap]"),c=t.querySelector("[data-toggle=handler-color]"),h=t.querySelector("[data-toggle=handler-drag]"),f=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,i=t.target.value;e.setColor(i,n),s&&(s.style.backgroundColor=i)};if(u&&(0,o.on)(u,"click",function(e){return e.stopPropagation()}),l&&(0,o.on)(l,"click",function(t){t.stopPropagation(),e.remove()}),c&&((0,o.on)(c,"change",f),(0,o.on)(c,"input",function(e){return f(e,0)})),h){var d=0,v=0,p=0,g={},y={},m={},k=function(t){var n=(0,o.getPointerEvent)(t);p=1,m.x=n.clientX-y.x,m.y=n.clientY-y.y,d=100*m.x,d/=g.w,d=v+d,d=da?a:d,e.setPosition(d,0),e.emit("handler:drag",e,d),(0,o.isDef)(t.button)&&0===t.which&&b(t)},b=function t(n){(0,o.off)(document,"touchmove mousemove",k),(0,o.off)(document,"touchend mouseup",t),p&&(p=0,e.setPosition(d),e.emit("handler:drag:end",e,d))},w=function(t){if(!(0,o.isDef)(t.button)||0===t.button){e.select();var i=(0,o.getPointerEvent)(t);v=e.position,g.w=n.clientWidth,g.h=n.clientHeight,y.x=i.clientX,y.y=i.clientY,(0,o.on)(document,"touchmove mousemove",k),(0,o.on)(document,"touchend mouseup",b),e.emit("handler:drag:start",e)}};(0,o.on)(h,"touchstart mousedown",w),(0,o.on)(h,"click",function(e){return e.stopPropagation()})}}},{key:"emit",value:function(){var e;(e=this.gp).emit.apply(e,arguments)}},{key:"render",value:function(){var e=this.gp,t=e.options,n=e.previewEl,i=e.colorPicker,r=t.pfx,o=t.colorEl,a=this.getColor();if(n){var l=document.createElement("div"),u=l.style,s=r+"-handler";return l.className=s,l.innerHTML='\n \n \n \n '+(o||'\n
\n \n
')+"\n
\n ",u.position="absolute",u.top=0,u.left=this.position+"%",n.appendChild(l),this.el=l,this.initEvents(),this.cpFn=i&&i(this),l}}}]),e}();t.default=a}])});
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Grapick Dev
8 |
9 |
10 |
11 |
12 |
13 |
100 |
101 |
Grapick
102 |
103 |
104 |
105 |
112 |
113 |
121 |
122 |
123 |
124 |
127 |
128 |
129 |
130 |
131 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grapick",
3 | "version": "0.1.13",
4 | "description": "Easy configurable gradient picker, with no dependencies",
5 | "author": "Artur Arseniev",
6 | "main": "dist/grapick.min.js",
7 | "license": "MIT",
8 | "keywords": [
9 | "gradient",
10 | "color",
11 | "picker"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/artf/grapick.git"
16 | },
17 | "scripts": {
18 | "lint": "eslint src",
19 | "build": "cross-env WEBPACK_ENV=prod && npm run lint && npm run test && npm run v:patch && webpack --display-modules && npm run build:css",
20 | "build:css": "node-sass src/styles/main.scss dist/grapick.min.css --output-style compressed",
21 | "v:patch": "npm version --no-git-tag-version patch",
22 | "start": "cross-env WEBPACK_ENV=dev webpack-dev-server --progress --colors & npm run build:css -- -w",
23 | "test": "jest --coverage",
24 | "test:dev": "npm test -- --watch"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.26.3",
28 | "babel-loader": "^7.1.5",
29 | "babel-preset-env": "^1.7.0",
30 | "cross-env": "^5.2.1",
31 | "eslint": "^4.19.1",
32 | "jest": "^21.0.2",
33 | "node-sass": "^4.14.1",
34 | "webpack": "^3.12.0",
35 | "webpack-dev-server": "^2.11.5"
36 | },
37 | "jest": {
38 | "collectCoverageFrom": [
39 | "src/**/*.js"
40 | ],
41 | "testRegex": "test/index.js"
42 | },
43 | "babel": {
44 | "presets": [
45 | [
46 | "env",
47 | {
48 | "targets": [
49 | "> 1%",
50 | "ie 11",
51 | "safari 8"
52 | ],
53 | "useBuiltIns": true
54 | }
55 | ]
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Grapick.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from './emitter'
2 | import Handler from './Handler'
3 | import { on } from './utils'
4 |
5 | const comparator = (l, r) => {
6 | return l.position - r.position;
7 | }
8 |
9 | const typeName = name => `${name}-gradient(`;
10 |
11 | /**
12 | * Main Grapick class
13 | * @extends EventEmitter
14 | */
15 | export default class Grapick extends EventEmitter {
16 |
17 | constructor(options = {}) {
18 | super();
19 | const pfx = 'grp';
20 | options = Object.assign({}, options);
21 | const defaults = {
22 | // Class prefix
23 | pfx,
24 |
25 | // HTMLElement/query string on which the gradient input will be attached
26 | el: `.${pfx}`,
27 |
28 | // Element to use for the custom color picker, eg. ''
29 | // Will be added inside the color picker container
30 | colorEl: '',
31 |
32 | // Minimum handler position, default: 0
33 | min: 0,
34 |
35 | // Maximum handler position, default: 100
36 | max: 100,
37 |
38 | // Any supported gradient direction: '90deg' (default), 'top', 'bottom', 'right', '135deg', etc.
39 | direction: '90deg',
40 |
41 | // Gradient type, available options: 'linear' (default) | 'radial' | 'repeating-linear' | 'repeating-radial'
42 | type: 'linear',
43 |
44 | // Gradient input height, default: '30px'
45 | height: '30px',
46 |
47 | // Gradient input width, default: '100%'
48 | width: '100%',
49 |
50 | // Default empty color (when you click on an empty color picker)
51 | emptyColor: '#000',
52 |
53 | // Format handler position value, default (to avoid floats): val => parseInt(val)
54 | onValuePos: val => parseInt(val),
55 | };
56 |
57 | for (let name in defaults) {
58 | if (!(name in options))
59 | options[name] = defaults[name];
60 | }
61 |
62 | let el = options.el;
63 | el = typeof el == 'string' ? document.querySelector(el) : el;
64 |
65 | if (!(el instanceof HTMLElement)) {
66 | throw `Element not found, given ${el}`;
67 | }
68 |
69 | this.el = el;
70 | this.handlers = [];
71 | this.options = options;
72 | this.on('handler:color:change', (h, c) => this.change(c));
73 | this.on('handler:position:change', (h, c) => this.change(c));
74 | this.on('handler:remove', h => this.change(1));
75 | this.on('handler:add', h => this.change(1));
76 | this.render();
77 | }
78 |
79 | /**
80 | * Destroy Grapick
81 | */
82 | destroy() {
83 | this.clear();
84 | this.e = {}; // Clear all events;
85 | [ // Clear the state
86 | 'el', 'handlers', 'options', 'colorPicker',
87 | ].forEach(i => this[i] = 0);
88 | [ // Remove DOM elements
89 | 'previewEl', 'wrapperEl', 'sandEl',
90 | ].forEach(key => {
91 | const el = this[key];
92 | el && el.parentNode && el.parentNode.removeChild(el);
93 | delete this[key];
94 | });
95 | }
96 |
97 |
98 | /**
99 | * Set custom color picker
100 | * @param {Object} cp Color picker interface
101 | * @example
102 | * const gp = new Grapick({
103 | * el: '#gp',
104 | * colorEl: ''
105 | * });
106 | * gp.setColorPicker(handler => {
107 | * const colorEl = handler.getEl().querySelector('#colorpicker');
108 | *
109 | * // Or you might face something like this
110 | * colorPicker({
111 | * el: colorEl,
112 | * startColoer: handler.getColor(),
113 | * change(color) {
114 | * handler.setColor(color);
115 | * }
116 | * });
117 | *
118 | * // jQuery style color picker
119 | * $(colorEl).colorPicker2({...}).on('change', () => {
120 | * handler.setColor(this.value);
121 | * })
122 | *
123 | * // In order to avoid memory leaks, return a function and call there
124 | * // the destroy method of you color picker instance
125 | * return () => {
126 | * // destroy your color picker instance
127 | * }
128 | * })
129 | */
130 | setColorPicker(cp) {
131 | this.colorPicker = cp;
132 | }
133 |
134 |
135 | /**
136 | * Get the complete style value
137 | * @return {string}
138 | * @example
139 | * const ga = new Grapick({...});
140 | * ga.addHandler(0, '#000');
141 | * ga.addHandler(55, 'white');
142 | * console.log(ga.getValue());
143 | * // -> `linear-gradient(left, #000 0%, white 55%)`
144 | */
145 | getValue(type, angle) {
146 | const color = this.getColorValue();
147 | const tp = type || this.getType();
148 | const defDir = ['top', 'left', 'bottom', 'right', 'center'];
149 | let ang = angle || this.getDirection();
150 |
151 | if (
152 | ['linear', 'repeating-linear'].indexOf(tp) >= 0
153 | && defDir.indexOf(ang) >= 0
154 | ) {
155 | ang = ang === 'center' ? 'to right' : `to ${ang}`;
156 | }
157 |
158 | if (
159 | ['radial', 'repeating-radial'].indexOf(tp) >= 0
160 | && defDir.indexOf(ang) >= 0
161 | ) {
162 | ang = `circle at ${ang}`;
163 | }
164 |
165 | return color ? `${tp}-gradient(${ang}, ${color})` : '';
166 | }
167 |
168 |
169 | /**
170 | * Get the gradient value with the browser prefix if necessary.
171 | * The usage of this method is deprecated (noticed weird behaviors in modern browsers).
172 | * @return {string}
173 | * @deprecated
174 | */
175 | getSafeValue(type, angle) {
176 | const previewEl = this.previewEl;
177 | const value = this.getValue(type, angle);
178 | !this.sandEl && (this.sandEl = document.createElement('div'))
179 |
180 | if (!previewEl || !value) {
181 | return '';
182 | }
183 |
184 | const style = this.sandEl.style;
185 | const values = [value, ...this.getPrefixedValues(type, angle)];
186 | let val;
187 |
188 | for (let i = 0; i < values.length; i++) {
189 | val = values[i];
190 | style.backgroundImage = val;
191 |
192 | if (style.backgroundImage == val) {
193 | break;
194 | }
195 | }
196 |
197 | return style.backgroundImage;
198 | }
199 |
200 |
201 | /**
202 | * Parse and apply the value to the picker
203 | * @param {string} value Any valid gradient string
204 | * @param {Object} [options={}] Options
205 | * @param {Boolean} [options.silent] Don't trigger events
206 | * @example
207 | * ga.setValue('linear-gradient(90deg, rgba(18, 215, 151, 0.75) 31.25%, white 85.1562%)');
208 | * ga.setValue('-webkit-radial-gradient(left, red 10%, blue 85%)');
209 | */
210 | setValue(value = '', options = {}) {
211 | let type = this.type;
212 | let direction = this.direction;
213 | let start = value.indexOf('(') + 1;
214 | let end = value.lastIndexOf(')');
215 | let gradients = value.substring(start, end);
216 | const values = gradients.split(/,(?![^(]*\)) /);
217 | this.clear(options);
218 |
219 | if (!gradients) {
220 | this.updatePreview();
221 | return;
222 | }
223 |
224 | if (values.length > 2) {
225 | direction = values.shift();
226 | }
227 |
228 | let typeFound;
229 | const types = ['repeating-linear', 'repeating-radial', 'linear', 'radial'];
230 | types.forEach(name => {
231 | if (value.indexOf(typeName(name)) > -1 && !typeFound) {
232 | typeFound = 1;
233 | type = name;
234 | }
235 | })
236 |
237 | this.setDirection(direction, options);
238 | this.setType(type, options);
239 | values.forEach(value => {
240 | const hdlValues = value.split(' ');
241 | const position = parseFloat(hdlValues.pop());
242 | const color = hdlValues.join('');
243 | this.addHandler(position, color, 0, options);
244 | })
245 | this.updatePreview();
246 | }
247 |
248 |
249 | /**
250 | * Get only colors value
251 | * @return {string}
252 | * @example
253 | * const ga = new Grapick({...});
254 | * ga.addHandler(0, '#000');
255 | * ga.addHandler(55, 'white');
256 | * console.log(ga.getColorValue());
257 | * // -> `#000 0%, white 55%`
258 | */
259 | getColorValue() {
260 | let handlers = this.handlers;
261 | handlers.sort(comparator);
262 | handlers = handlers.length == 1 ? [handlers[0], handlers[0]] : handlers;
263 | return handlers.map(handler => handler.getValue()).join(', ');
264 | }
265 |
266 |
267 | /**
268 | * Get an array with browser specific values
269 | * @return {Array}
270 | * @example
271 | * const ga = new Grapick({...});
272 | * ga.addHandler(0, '#000');
273 | * ga.addHandler(55, 'white');
274 | * console.log(ga.getPrefixedValues());
275 | * // -> [
276 | * "-moz-linear-gradient(left, #000 0%, white 55%)",
277 | * "-webkit-linear-gradient(left, #000 0%, white 55%)"
278 | * "-o-linear-gradient(left, #000 0%, white 55%)"
279 | * ]
280 | */
281 | getPrefixedValues(type, angle) {
282 | const value = this.getValue(type, angle);
283 | return ['-moz-', '-webkit-', '-o-', '-ms-'].map(prefix =>
284 | `${prefix}${value}`);
285 | }
286 |
287 |
288 | /**
289 | * Trigger change
290 | * @param {Boolean} complete Indicates if the change is complete (eg. while dragging is not complete)
291 | * @param {Object} [options={}] Options
292 | * @param {Boolean} [options.silent] Don't trigger events
293 | */
294 | change(complete = 1, options = {}) {
295 | this.updatePreview();
296 | !options.silent && this.emit('change', complete);
297 | // TODO can't make it work with jsdom
298 | //timerChange && clearTimeout(timerChange);
299 | //timerChange = setTimeout(() => this.emit('change', complete), 0);
300 | }
301 |
302 |
303 | /**
304 | * Set gradient direction, eg. 'top', 'left', 'bottom', 'right', '90deg', etc.
305 | * @param {string} direction Any supported direction
306 | * @param {Object} [options={}] Options
307 | * @param {Boolean} [options.silent] Don't trigger events
308 | */
309 | setDirection(direction, options = {}) {
310 | this.options.direction = direction;
311 | const { complete = 1 } = options;
312 | this.change(complete, options);
313 | }
314 |
315 |
316 | /**
317 | * Set gradient direction, eg. 'top', 'left', 'bottom', 'right', '90deg', etc.
318 | * @param {string} direction Any supported direction
319 | */
320 | getDirection() {
321 | return this.options.direction;
322 | }
323 |
324 |
325 | /**
326 | * Set gradient type, available options: 'linear' or 'radial'
327 | * @param {string} direction Any supported direction
328 | * @param {Object} [options={}] Options
329 | * @param {Boolean} [options.silent] Don't trigger events
330 | */
331 | setType(type, options = {}) {
332 | this.options.type = type;
333 | const { complete = 1 } = options;
334 | this.change(complete, options);
335 | }
336 |
337 |
338 | /**
339 | * Get gradient type
340 | * @return {string}
341 | */
342 | getType() {
343 | return this.options.type;
344 | }
345 |
346 |
347 | /**
348 | * Add gradient handler
349 | * @param {integer} position Position integer in percentage
350 | * @param {string} color Color string, eg. red, #123, 'rgba(30,87,153,1)', etc..
351 | * @param {Boolean} select Select the handler once it's added
352 | * @param {Object} [options={}] Handler options
353 | * @param {Boolean} [options.silent] Don't trigger events
354 | * @return {Object} Handler object
355 | */
356 | addHandler(position, color, select = 1, options = {}) {
357 | const handler = new Handler(this, position, color, select, options);
358 | !options.silent && this.emit('handler:add', handler);
359 | return handler;
360 | }
361 |
362 |
363 | /**
364 | * Get handler by index
365 | * @param {integer} index
366 | * @return {Object}
367 | */
368 | getHandler(index) {
369 | return this.handlers[index];
370 | }
371 |
372 |
373 | /**
374 | * Get all handlers
375 | * @return {Array}
376 | */
377 | getHandlers() {
378 | return this.handlers;
379 | }
380 |
381 |
382 | /**
383 | * Remove all handlers
384 | * @param {Object} [options={}] Options
385 | * @param {Boolean} [options.silent] Don't trigger events
386 | * @example
387 | * ga.clear();
388 | * // Don't trigger events
389 | * ga.clear({silent: 1});
390 | */
391 | clear(options = {}) {
392 | const handlers = this.handlers;
393 |
394 | for (var i = handlers.length - 1; i >= 0; i--) {
395 | handlers[i].remove(options);
396 | }
397 | }
398 |
399 |
400 | /**
401 | * Return selected handler if one exists
402 | * @return {Handler|null}
403 | */
404 | getSelected() {
405 | const handlers = this.getHandlers();
406 |
407 | for (let i = 0; i < handlers.length; i++) {
408 | let handler = handlers[i];
409 |
410 | if (handler.isSelected()) {
411 | return handler;
412 | }
413 | }
414 |
415 | return null;
416 | }
417 |
418 |
419 | /**
420 | * Update preview element
421 | */
422 | updatePreview() {
423 | const previewEl = this.previewEl;
424 | previewEl && (previewEl.style.backgroundImage = this.getValue('linear', 'to right'));
425 | }
426 |
427 |
428 | initEvents() {
429 | const pEl = this.previewEl;
430 | pEl && on(pEl, 'click', e => {
431 | // First of all, find a position of the click in percentage
432 | const opt = this.options;
433 | const { min, max } = opt;
434 | const elDim = {
435 | w: pEl.clientWidth,
436 | h: pEl.clientHeight,
437 | };
438 | const x = e.offsetX - pEl.clientLeft;
439 | const y = e.offsetY - pEl.clientTop;
440 | const percentage = x / elDim.w * 100;
441 |
442 | if (
443 | percentage > max || percentage < min ||
444 | y > elDim.h || y < 0
445 | ) {
446 | return;
447 | }
448 |
449 | // Now let's find the pixel color by using the canvas
450 | let canvas = document.createElement('canvas');
451 | let context = canvas.getContext('2d');
452 | canvas.width = elDim.w;
453 | canvas.height = elDim.h;
454 | let grad = context.createLinearGradient(0, 0, elDim.w, elDim.h);
455 | this.getHandlers().forEach(h => grad.addColorStop(h.position/100, h.color));
456 | context.fillStyle = grad;
457 | context.fillRect(0, 0, canvas.width, canvas.height);
458 | canvas.style.background = 'black';
459 | const rgba = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
460 | const color = `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
461 | const fc = color == 'rgba(0, 0, 0, 0)' ? opt.emptyColor : color;
462 | this.addHandler(percentage, fc);
463 | });
464 | }
465 |
466 |
467 | /**
468 | * Render the gradient picker
469 | */
470 | render() {
471 | const opt = this.options;
472 | const el = this.el;
473 | const pfx = opt.pfx;
474 | const height = opt.height;
475 | const width = opt.width;
476 |
477 | if (!el) {
478 | return;
479 | }
480 |
481 | const wrapperCls = `${pfx}-wrapper`;
482 | const previewCls = `${pfx}-preview`;
483 | el.innerHTML = `
484 |
487 | `;
488 | const wrapperEl = el.querySelector(`.${wrapperCls}`);
489 | const previewEl = el.querySelector(`.${previewCls}`);
490 | const styleWrap = wrapperEl.style;
491 | styleWrap.position = 'relative';
492 | this.wrapperEl = wrapperEl;
493 | this.previewEl = previewEl;
494 |
495 | if (height) {
496 | styleWrap.height = height;
497 | }
498 |
499 | if (width) {
500 | styleWrap.width = width;
501 | }
502 |
503 | this.initEvents();
504 | this.updatePreview();
505 | }
506 | }
507 |
--------------------------------------------------------------------------------
/src/Handler.js:
--------------------------------------------------------------------------------
1 | import {on, off, isFunction, isDef, getPointerEvent } from './utils'
2 |
3 | /**
4 | * Handler is the color stop of the gradient
5 | */
6 | export default class Handler {
7 |
8 | constructor(Grapick, position = 0, color = 'black', select = 1, opts = {}) {
9 | Grapick.getHandlers().push(this);
10 | this.gp = Grapick;
11 | this.position = position;
12 | this.color = color;
13 | this.selected = 0;
14 | this.render();
15 | select && this.select(opts);
16 | }
17 |
18 | toJSON() {
19 | return {
20 | position: this.position,
21 | selected: this.selected,
22 | color: this.color,
23 | };
24 | }
25 |
26 | /**
27 | * Set color
28 | * @param {string} color Color string, eg. red, #123, 'rgba(30,87,153,1)', etc..
29 | * @param {Boolean} complete Indicates if the action is complete
30 | * @example
31 | * handler.setColor('red');
32 | */
33 | setColor(color, complete = 1) {
34 | this.color = color;
35 | this.emit('handler:color:change', this, complete);
36 | }
37 |
38 | /**
39 | * Set color
40 | * @param {integer} position Position integer in percentage, eg. 20, 50, 100
41 | * @param {Boolean} complete Indicates if the action is complete
42 | * @example
43 | * handler.setPosition(55);
44 | */
45 | setPosition(position, complete = 1) {
46 | const el = this.getEl();
47 | this.position = position;
48 | el && (el.style.left = `${position}%`);
49 | this.emit('handler:position:change', this, complete);
50 | }
51 |
52 | /**
53 | * Get color of the handler
54 | * @return {string} Color string
55 | */
56 | getColor() {
57 | return this.color;
58 | }
59 |
60 | /**
61 | * Get position of the handler
62 | * @return {integer} Position integer
63 | */
64 | getPosition() {
65 | const { position, gp } = this;
66 | const { onValuePos } = gp.options;
67 | return isFunction(onValuePos) ? onValuePos(position) : position;
68 | }
69 |
70 | /**
71 | * Indicates if the handler is the selected one
72 | * @return {Boolean}
73 | */
74 | isSelected() {
75 | return !!this.selected;
76 | }
77 |
78 | /**
79 | * Get value of the handler
80 | * @return {string}
81 | * @example
82 | * handler.getValue(); // -> `black 0%`
83 | */
84 | getValue() {
85 | return `${this.getColor()} ${this.getPosition()}%`;
86 | }
87 |
88 | /**
89 | * Select the current handler and deselect others
90 | * @param {Object} [options={}] Options
91 | * @param {Boolean} [options.keepSelect=false] Avoid deselecting other handlers
92 | */
93 | select(opts = {}) {
94 | const el = this.getEl();
95 | const handlers = this.gp.getHandlers();
96 | !opts.keepSelect && handlers.forEach(handler => handler.deselect());
97 | this.selected = 1;
98 | const clsNameSel = this.getSelectedCls();
99 | el && (el.className += ` ${clsNameSel}`);
100 | this.emit('handler:select', this);
101 | }
102 |
103 | /**
104 | * Deselect the current handler
105 | */
106 | deselect() {
107 | const el = this.getEl();
108 | this.selected = 0;
109 | const clsNameSel = this.getSelectedCls();
110 | el && (el.className = el.className.replace(clsNameSel, '').trim());
111 | this.emit('handler:deselect', this);
112 | }
113 |
114 | getSelectedCls() {
115 | const pfx = this.gp.options.pfx;
116 | return `${pfx}-handler-selected`;
117 | }
118 |
119 | /**
120 | * Remove the current handler
121 | * @param {Object} [options={}] Options
122 | * @param {Boolean} [options.silent] Don't trigger events
123 | * @return {Handler} Removed handler (itself)
124 | */
125 | remove(options = {}) {
126 | const { cpFn } = this;
127 | const el = this.getEl();
128 | const handlers = this.gp.getHandlers();
129 | const removed = handlers.splice(handlers.indexOf(this), 1)[0];
130 | el && el.parentNode.removeChild(el);
131 | !options.silent && this.emit('handler:remove', removed);
132 | isFunction(cpFn) && cpFn(this);
133 | ['el', 'gp'].forEach(i => this[i] = 0);
134 | return removed;
135 | }
136 |
137 | /**
138 | * Get handler element
139 | * @return {HTMLElement}
140 | */
141 | getEl() {
142 | return this.el;
143 | }
144 |
145 | initEvents() {
146 | const eventDown = 'touchstart mousedown';
147 | const eventMove = 'touchmove mousemove';
148 | const eventUp = 'touchend mouseup';
149 | const el = this.getEl();
150 | const previewEl = this.gp.previewEl;
151 | const options = this.gp.options;
152 | const min = options.min;
153 | const max = options.max;
154 | const closeEl = el.querySelector('[data-toggle=handler-close]');
155 | const colorContEl = el.querySelector('[data-toggle=handler-color-c]');
156 | const colorWrapEl = el.querySelector('[data-toggle=handler-color-wrap]');
157 | const colorEl = el.querySelector('[data-toggle=handler-color]');
158 | const dragEl = el.querySelector('[data-toggle=handler-drag]');
159 | const upColor = (ev, complete = 1) => {
160 | const { value } = ev.target;
161 | this.setColor(value, complete);
162 | colorWrapEl && (colorWrapEl.style.backgroundColor = value);
163 | }
164 | colorContEl && on(colorContEl, 'click', e => e.stopPropagation());
165 | closeEl && on(closeEl, 'click', e => {
166 | e.stopPropagation();
167 | this.remove()
168 | });
169 | if (colorEl) {
170 | on(colorEl, 'change', upColor);
171 | on(colorEl, 'input', ev => upColor(ev, 0));
172 | }
173 |
174 | if (dragEl) {
175 | let pos = 0;
176 | let posInit = 0;
177 | let dragged = 0;
178 | const elDim = {};
179 | const startPos = {};
180 | const deltaPos = {};
181 | const axis = 'x';
182 | const drag = e => {
183 | const evP = getPointerEvent(e);
184 | dragged = 1;
185 | deltaPos.x = evP.clientX - startPos.x;
186 | deltaPos.y = evP.clientY - startPos.y;
187 | pos = (axis == 'x' ? deltaPos.x : deltaPos.y) * 100;
188 | pos = pos / (axis == 'x' ? elDim.w : elDim.h);
189 | pos = posInit + pos;
190 | pos = pos < min ? min : pos;
191 | pos = pos > max ? max : pos;
192 | this.setPosition(pos, 0);
193 | this.emit('handler:drag', this, pos);
194 | // In case the mouse button was released outside of the window
195 | isDef(e.button) && e.which === 0 && stopDrag(e);
196 | };
197 | const stopDrag = e => {
198 | off(document, eventMove, drag);
199 | off(document, eventUp, stopDrag);
200 | if (!dragged) {
201 | return;
202 | }
203 | dragged = 0;
204 | this.setPosition(pos);
205 | this.emit('handler:drag:end', this, pos);
206 | };
207 | const initDrag = e => {
208 | //Right or middel click
209 | if (isDef(e.button) && e.button !== 0) {
210 | return;
211 | }
212 | this.select();
213 | const evP = getPointerEvent(e);
214 | posInit = this.position;
215 | elDim.w = previewEl.clientWidth;
216 | elDim.h = previewEl.clientHeight;
217 | startPos.x = evP.clientX;
218 | startPos.y = evP.clientY;
219 | on(document, eventMove, drag);
220 | on(document, eventUp, stopDrag);
221 | this.emit('handler:drag:start', this);
222 | };
223 |
224 | on(dragEl, eventDown, initDrag);
225 | on(dragEl, 'click', e => e.stopPropagation());
226 | }
227 | }
228 |
229 | emit() {
230 | this.gp.emit(...arguments);
231 | }
232 |
233 | /**
234 | * Render the handler
235 | * @return {HTMLElement} Rendered element
236 | */
237 | render() {
238 | const gp = this.gp;
239 | const opt = gp.options;
240 | const previewEl = gp.previewEl;
241 | const colorPicker = gp.colorPicker;
242 | const pfx = opt.pfx;
243 | const colorEl = opt.colorEl;
244 | const color = this.getColor();
245 |
246 | if (!previewEl) {
247 | return;
248 | }
249 |
250 | const hEl = document.createElement('div');
251 | const style = hEl.style;
252 | const baseCls = `${pfx}-handler`;
253 | hEl.className = baseCls;
254 | hEl.innerHTML = `
255 |
258 |
259 |
260 | ${colorEl || `
261 |
262 |
263 |
`}
264 |
265 | `;
266 | style.position = 'absolute';
267 | style.top = 0;
268 | style.left = `${this.position}%`;
269 | previewEl.appendChild(hEl);
270 | this.el = hEl;
271 | this.initEvents();
272 | this.cpFn = colorPicker && colorPicker(this);
273 | return hEl;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/src/emitter.js:
--------------------------------------------------------------------------------
1 | export default class EventEmitter {
2 | on(name, callback, ctx) {
3 | var e = this.e || (this.e = {});
4 |
5 | (e[name] || (e[name] = [])).push({
6 | fn: callback,
7 | ctx: ctx
8 | });
9 |
10 | return this;
11 | }
12 |
13 | once(name, callback, ctx) {
14 | var self = this;
15 | function listener () {
16 | self.off(name, listener);
17 | callback.apply(ctx, arguments);
18 | };
19 |
20 | listener._ = callback
21 | return this.on(name, listener, ctx);
22 | }
23 |
24 | emit(name) {
25 | var data = [].slice.call(arguments, 1);
26 | var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
27 | var i = 0;
28 | var len = evtArr.length;
29 |
30 | for (i; i < len; i++) {
31 | evtArr[i].fn.apply(evtArr[i].ctx, data);
32 | }
33 |
34 | return this;
35 | }
36 |
37 | off(name, callback) {
38 | var e = this.e || (this.e = {});
39 | var evts = e[name];
40 | var liveEvents = [];
41 |
42 | if (evts && callback) {
43 | for (var i = 0, len = evts.length; i < len; i++) {
44 | if (evts[i].fn !== callback && evts[i].fn._ !== callback)
45 | liveEvents.push(evts[i]);
46 | }
47 | }
48 |
49 | // Remove event from queue to prevent memory leak
50 | // Suggested by https://github.com/lazd
51 | // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
52 |
53 | (liveEvents.length)
54 | ? e[name] = liveEvents
55 | : delete e[name];
56 |
57 | return this;
58 | }
59 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Grapick from './Grapick'
2 |
3 | module.exports = (o) => new Grapick(o)
4 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | $grp-pfx: 'grp-' !default;
2 | $grp-close-dim: 15px !default;
3 | $grp-cp-dim: $grp-close-dim !default;
4 | $grp-stop-bg: white !default;
5 | $grp-cp-border: 3px solid $grp-stop-bg !default;
6 | $grp-stop-radius: 100% !default;
7 | $grp-stop-shadow: 0 2px 10px rgba(0, 0, 0, 0.25) !default;
8 | $grp-preview-bg: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==";
9 |
10 | .#{$grp-pfx} {
11 | &wrapper {
12 | background-image: url($grp-preview-bg);
13 | }
14 |
15 | &preview {
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | width: 100%;
20 | height: 100%;
21 | cursor: crosshair;
22 | }
23 |
24 | &handler {
25 | width: 4px;
26 | margin-left: -2px;
27 | user-select: none;
28 | -webkit-user-select: none;
29 | -moz-user-select: none;
30 | height: 100%;
31 |
32 | &-close {
33 | color: rgba(0, 0, 0, 0.4);
34 | border-radius: $grp-stop-radius;
35 | box-shadow: $grp-stop-shadow;
36 | background-color: $grp-stop-bg;
37 | text-align: center;
38 | width: $grp-close-dim;
39 | height: $grp-close-dim;
40 | margin-left: -($grp-close-dim/3);
41 | line-height: 10px;
42 | font-size: 21px;
43 | cursor: pointer;
44 |
45 | &-c {
46 | position: absolute;
47 | top: -($grp-close-dim + 2px);
48 | }
49 | }
50 |
51 | &-drag {
52 | background-color: rgba(0, 0, 0, 0.5);
53 | cursor: col-resize;
54 | width: 100%;
55 | height: 100%;
56 |
57 | .#{$grp-pfx}handler-selected & {
58 | background-color: rgba(255, 255, 255, 0.5);
59 | }
60 | }
61 |
62 | &-cp {
63 | &-c {
64 | display: none;
65 |
66 | .#{$grp-pfx}handler-selected & {
67 | display: block;
68 | }
69 | }
70 |
71 | &-wrap {
72 | width: $grp-cp-dim;
73 | height: $grp-cp-dim;
74 | margin-left: -($grp-cp-dim/2 + 0.5px);
75 | border: $grp-cp-border;
76 | box-shadow: $grp-stop-shadow;
77 | overflow: hidden;
78 | border-radius: $grp-stop-radius;
79 | cursor: pointer;
80 |
81 | input[type=color] {
82 | opacity: 0;
83 | cursor: pointer;
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function on(el, ev, fn) {
2 | ev = ev.split(/\s+/);
3 |
4 | for (let i = 0; i < ev.length; ++i) {
5 | el.addEventListener(ev[i], fn);
6 | }
7 | }
8 |
9 | export function off(el, ev, fn) {
10 | ev = ev.split(/\s+/);
11 |
12 | for (let i = 0; i < ev.length; ++i) {
13 | el.removeEventListener(ev[i], fn);
14 | }
15 | }
16 |
17 | export const isFunction = fn => typeof fn === 'function';
18 |
19 | export const isDef = val => typeof val !== 'undefined';
20 |
21 | export const getPointerEvent = ev => (ev.touches && ev.touches[0]) || ev;
22 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import Grapick from './../src';
2 | import Handler from './../src/Handler';
3 |
4 | let ga;
5 | let gah;
6 | let changed;
7 |
8 | describe('Grapick', () => {
9 |
10 | it('Main object is defined', () => {
11 | expect(Grapick).toBeDefined();
12 | });
13 |
14 | it('Throw error with no element', () => {
15 | expect(() => new Grapick()).toThrow();
16 | expect(() => new Grapick({})).toThrow();
17 | });
18 |
19 | describe('Startup', () => {
20 |
21 | beforeAll(() => {
22 | document.body.innerHTML = ``;
23 | });
24 |
25 | afterAll(() => {
26 | document.body.innerHTML = '';
27 | });
28 |
29 | beforeEach(() => {
30 | ga = new Grapick({el: '#gp'});
31 | changed = 0;
32 | ga.on('change', () => changed++);
33 | });
34 |
35 | it('Able to change type', () => {
36 | ga.setType('radial');
37 | expect(ga.getType()).toBe('radial');
38 | });
39 |
40 | it('Change of the type triggers the `change` event', () => {
41 | ga.setType('radial');
42 | expect(changed).toBe(1);
43 | });
44 |
45 | it('Default type is linear', () => {
46 | expect(ga.getType()).toBe('linear');
47 | });
48 |
49 | it('Default direction is 90deg', () => {
50 | expect(ga.getDirection()).toBe('90deg');
51 | });
52 |
53 | it('Able to change direction', () => {
54 | ga.setDirection('top');
55 | expect(ga.getDirection()).toBe('top');
56 | });
57 |
58 | it('Change of the direction triggers the `change` event', () => {
59 | ga.setDirection('top');
60 | expect(changed).toBe(1);
61 | });
62 |
63 | it('Has no handlers', () => {
64 | expect(ga.getHandlers().length).toBe(0);
65 | });
66 |
67 | it('Return undefined handler', () => {
68 | expect(ga.getHandler(0)).toBeUndefined();
69 | });
70 |
71 | it('Add handler', () => {
72 | const h = ga.addHandler(50, '#fff');
73 | expect(h).toBeDefined();
74 | expect(h instanceof Handler).toBe(true);
75 | expect(h.getColor()).toBe('#fff');
76 | expect(h.isSelected()).toBe(true);
77 | expect(ga.getHandlers().length).toBe(1);
78 | });
79 |
80 | it('Add handler triggers change', () => {
81 | const h = ga.addHandler(50, '#fff');
82 | expect(changed).toBe(1);
83 | });
84 |
85 | it('Get color values from single handler', () => {
86 | ga.addHandler(0, '#000');
87 | // Prevent error with one single handler
88 | expect(ga.getColorValue()).toBe('#000 0%, #000 0%');
89 | });
90 |
91 | it('Get color values from more handlers', () => {
92 | ga.addHandler(0, '#000');
93 | ga.addHandler(55, 'white');
94 | ga.addHandler(100, 'rgba(11, 23, 44, 1)');
95 | expect(ga.getColorValue()).toBe('#000 0%, white 55%, rgba(11, 23, 44, 1) 100%');
96 | });
97 |
98 | it('Get value', () => {
99 | ga.addHandler(0, '#000');
100 | ga.addHandler(55, 'white');
101 | expect(ga.getValue()).toBe('linear-gradient(90deg, #000 0%, white 55%)');
102 | });
103 |
104 | it('Get value with passed type and angle', () => {
105 | ga.addHandler(0, '#000');
106 | ga.addHandler(55, 'white');
107 | expect(ga.getValue('radial', 'center')).toBe('radial-gradient(circle at center, #000 0%, white 55%)');
108 | });
109 |
110 | it.skip('Get safe value', () => {
111 | ga.addHandler(0, '#000');
112 | ga.addHandler(55, 'white');
113 | ga.setDirection('left');
114 | expect(ga.getSafeValue()).toContain('linear-gradient(left, #000 0%, white 55%)');
115 | });
116 |
117 | it('Get prefixed values', () => {
118 | ga.addHandler(0, '#000');
119 | ga.addHandler(55, 'white');
120 | expect(ga.getPrefixedValues()).toEqual([
121 | '-moz-linear-gradient(90deg, #000 0%, white 55%)',
122 | '-webkit-linear-gradient(90deg, #000 0%, white 55%)',
123 | '-o-linear-gradient(90deg, #000 0%, white 55%)',
124 | '-ms-linear-gradient(90deg, #000 0%, white 55%)'
125 | ]);
126 | });
127 |
128 | it('change() triggers the `change` event', () => {
129 | ga.change();
130 | expect(changed).toBe(1);
131 | });
132 |
133 | it('clear() works', () => {
134 | ga.addHandler(0, '#000');
135 | ga.addHandler(55, 'white');
136 | expect(ga.getHandlers().length).toBe(2);
137 | ga.clear();
138 | expect(ga.getHandlers().length).toBe(0);
139 | expect(changed).toBe(4); // TODO to fix (issues with jsdom), should be 1
140 | });
141 |
142 | it('getSelected() works', () => {
143 | const h = ga.addHandler(0, '#000', 1);
144 | ga.addHandler(55, 'white', 0);
145 | expect(ga.getSelected()).toBe(h);
146 | });
147 |
148 | it.skip('Click on preview element adds new handler', () => {
149 | // Have to mock previewEl and click event
150 | ga.previewEl.click();
151 | expect(ga.getHandlers().length).toBe(1);
152 | });
153 |
154 | it('setValue() with linear string works', () => {
155 | ga.setValue('radial-gradient(77deg, rgba(18, 215, 151, 0.75) 31.25%, white 85.1562%)');
156 | expect(!!changed).toBe(true);
157 | expect(ga.getType()).toBe('radial');
158 | expect(ga.getDirection()).toBe('77deg');
159 | expect(JSON.parse(JSON.stringify(ga.getHandlers()))).toEqual([
160 | { color: 'rgba(18,215,151,0.75)', position: 31.25, selected: 0},
161 | { color: 'white', position: 85.1562, selected: 0}
162 | ]);
163 | });
164 |
165 | it('silent setValue() works', () => {
166 | ga.setValue('linear-gradient(left, red 0%, blue 100%)', { silent: 1 });
167 | expect(!!changed).toBe(false);
168 | });
169 | });
170 |
171 | describe('Grapick Handler', () => {
172 | beforeAll(() => {
173 | document.body.innerHTML = ``;
174 | });
175 |
176 | afterAll(() => {
177 | document.body.innerHTML = '';
178 | });
179 |
180 | beforeEach(() => {
181 | ga = new Grapick({el: '#gp'});
182 | gah = new Handler(ga);
183 | });
184 |
185 | it('Handler exists', () => {
186 | expect(gah).toBeDefined();
187 | expect(ga.getHandlers().length).toBe(1);
188 | });
189 |
190 | it('Check default selection', () => {
191 | expect(gah.isSelected()).toBe(true);
192 | });
193 |
194 | it('Check default position', () => {
195 | expect(gah.getPosition()).toBe(0);
196 | });
197 |
198 | it('Check default color', () => {
199 | expect(gah.getColor()).toBe('black');
200 | });
201 |
202 | it('Check default value', () => {
203 | expect(gah.getValue()).toBe('black 0%');
204 | });
205 |
206 | it('Able to change color', () => {
207 | gah.setColor('red');
208 | expect(gah.getColor()).toBe('red');
209 | });
210 |
211 | it('Able to change position', () => {
212 | gah.setPosition(55);
213 | expect(gah.getPosition()).toBe(55);
214 | });
215 |
216 | it('Able to select', () => {
217 | gah.select();
218 | expect(gah.isSelected()).toBe(true);
219 | });
220 |
221 | it('Able to deselect', () => {
222 | gah.select();
223 | expect(gah.isSelected()).toBe(true);
224 | gah.deselect();
225 | expect(gah.isSelected()).toBe(false);
226 | });
227 |
228 | it('Able to remove', () => {
229 | const handler = gah.remove();
230 | expect(ga.getHandlers().length).toBe(0);
231 | expect(handler).toBe(gah);
232 | });
233 |
234 | it('Able to change pos', () => {
235 | expect(gah).toBeDefined();
236 | });
237 | })
238 |
239 | });
240 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var pkg = require('./package.json');
3 | var env = process.env.WEBPACK_ENV;
4 | var name = 'grapick';
5 | var plugins = [];
6 |
7 | if(env !== 'dev') {
8 | plugins = [
9 | new webpack.optimize.ModuleConcatenationPlugin(),
10 | new webpack.optimize.UglifyJsPlugin({minimize: true, compressor: {warnings: false}}),
11 | new webpack.BannerPlugin(pkg.name + ' - ' + pkg.version),
12 | ]
13 | }
14 |
15 | module.exports = {
16 | entry: './src',
17 | output: {
18 | filename: './dist/' + name + '.min.js',
19 | library: 'Grapick',
20 | libraryTarget: 'umd',
21 | },
22 | plugins: plugins,
23 | module: {
24 | loaders: [{
25 | test: /\.js$/,
26 | loader: 'babel-loader',
27 | include: /src/,
28 | exclude: /node_modules/
29 | }],
30 | },
31 | resolve: {
32 | modules: ['node_modules'],
33 | },
34 | }
35 |
--------------------------------------------------------------------------------