├── .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
\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
\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 |
485 |
486 |
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 |
256 |
257 |
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 | --------------------------------------------------------------------------------