1 angular.module('colorpicker.module', [])
2 .factory('Helper', function () {
5 closestSlider: function (elem) {
6 var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
7 if (matchesSelector.bind(elem)('I')) {
8 return elem.parentNode;
12 getOffset: function (elem, fixedPosition) {
16 rect = elem.getBoundingClientRect();
17 while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
18 if (!fixedPosition && elem.tagName === 'BODY') {
19 scrollX += document.documentElement.scrollLeft || elem.scrollLeft;
20 scrollY += document.documentElement.scrollTop || elem.scrollTop;
22 scrollX += elem.scrollLeft;
23 scrollY += elem.scrollTop;
25 elem = elem.offsetParent;
28 top: rect.top + window.pageYOffset,
29 left: rect.left + window.pageXOffset,
34 // a set of RE's that can match strings and generate color tuples. https://github.com/jquery/jquery-color/
37 re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
38 parse: function (execResult) {
48 re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
49 parse: function (execResult) {
59 re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
60 parse: function (execResult) {
62 parseInt(execResult[1], 16),
63 parseInt(execResult[2], 16),
64 parseInt(execResult[3], 16)
69 re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
70 parse: function (execResult) {
72 parseInt(execResult[1] + execResult[1], 16),
73 parseInt(execResult[2] + execResult[2], 16),
74 parseInt(execResult[3] + execResult[3], 16)
81 .factory('Color', ['Helper', function (Helper) {
90 // translate a format from Color object to a string
92 var rgb = this.toRGB();
93 return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
96 var rgb = this.toRGB();
97 return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + rgb.a + ')';
103 // HSBtoRGB from RaphaelJS
104 RGBtoHSB: function (r, g, b, a) {
110 V = Math.max(r, g, b);
111 C = V - Math.min(r, g, b);
112 H = (C === 0 ? null :
113 V === r ? (g - b) / C :
114 V === g ? (b - r) / C + 2 :
117 H = ((H + 360) % 6) * 60 / 360;
118 S = C === 0 ? 0 : C / V;
119 return {h: H || 1, s: S, b: V, a: a || 1};
122 //parse a string to HSB
123 setColor: function (val) {
124 val = (val) ? val.toLowerCase() : val;
125 for (var key in Helper.stringParsers) {
126 if (Helper.stringParsers.hasOwnProperty(key)) {
127 var parser = Helper.stringParsers[key];
128 var match = parser.re.exec(val),
129 values = match && parser.parse(match);
131 this.value = this.RGBtoHSB.apply(null, values);
138 setHue: function (h) {
139 this.value.h = 1 - h;
142 setSaturation: function (s) {
146 setLightness: function (b) {
147 this.value.b = 1 - b;
150 setAlpha: function (a) {
151 this.value.a = parseInt((1 - a) * 100, 10) / 100;
154 // HSBtoRGB from RaphaelJS
155 // https://github.com/DmitryBaranovskiy/raphael/
156 toRGB: function (h, s, b, a) {
166 X = C * (1 - Math.abs(h % 2 - 1));
170 R += [C, X, 0, 0, X, C][h];
171 G += [X, C, C, X, 0, 0][h];
172 B += [0, 0, X, C, C, X][h];
174 r: Math.round(R * 255),
175 g: Math.round(G * 255),
176 b: Math.round(B * 255),
181 toHex: function (h, s, b, a) {
182 var rgb = this.toRGB(h, s, b, a);
183 return '#' + ((1 << 24) | (parseInt(rgb.r, 10) << 16) | (parseInt(rgb.g, 10) << 8) | parseInt(rgb.b, 10)).toString(16).substr(1);
187 .factory('Slider', ['Helper', function (Helper) {
203 getSlider: function() {
206 getLeftPosition: function(event) {
207 return Math.max(0, Math.min(slider.maxLeft, slider.left + ((event.pageX || pointer.left) - pointer.left)));
209 getTopPosition: function(event) {
210 return Math.max(0, Math.min(slider.maxTop, slider.top + ((event.pageY || pointer.top) - pointer.top)));
212 setSlider: function (event, fixedPosition) {
214 target = Helper.closestSlider(event.target),
215 targetOffset = Helper.getOffset(target, fixedPosition),
216 rect = target.getBoundingClientRect(),
217 offsetX = event.clientX - rect.left,
218 offsetY = event.clientY - rect.top;
220 slider.knob = target.children[0].style;
221 slider.left = event.pageX - targetOffset.left - window.pageXOffset + targetOffset.scrollX;
222 slider.top = event.pageY - targetOffset.top - window.pageYOffset + targetOffset.scrollY;
225 left: event.pageX - (offsetX - slider.left),
226 top: event.pageY - (offsetY - slider.top)
229 setSaturation: function(event, fixedPosition, componentSize) {
231 maxLeft: componentSize,
232 maxTop: componentSize,
233 callLeft: 'setSaturation',
234 callTop: 'setLightness'
236 this.setSlider(event, fixedPosition);
238 setHue: function(event, fixedPosition, componentSize) {
241 maxTop: componentSize,
245 this.setSlider(event, fixedPosition);
247 setAlpha: function(event, fixedPosition, componentSize) {
250 maxTop: componentSize,
254 this.setSlider(event, fixedPosition);
256 setKnob: function(top, left) {
257 slider.knob.top = top + 'px';
258 slider.knob.left = left + 'px';
262 .directive('colorpicker', ['$document', '$compile', 'Color', 'Slider', 'Helper', function ($document, $compile, Color, Slider, Helper) {
267 link: function ($scope, elem, attrs, ngModel) {
269 thisFormat = attrs.colorpicker ? attrs.colorpicker : 'hex',
270 position = angular.isDefined(attrs.colorpickerPosition) ? attrs.colorpickerPosition : 'bottom',
271 inline = angular.isDefined(attrs.colorpickerInline) ? attrs.colorpickerInline : false,
272 fixedPosition = angular.isDefined(attrs.colorpickerFixedPosition) ? attrs.colorpickerFixedPosition : false,
273 target = angular.isDefined(attrs.colorpickerParent) ? elem.parent() : angular.element(document.body),
274 withInput = angular.isDefined(attrs.colorpickerWithInput) ? attrs.colorpickerWithInput : false,
275 componentSize = angular.isDefined(attrs.colorpickerSize) ? attrs.colorpickerSize : 100,
276 componentSizePx = componentSize + 'px',
277 inputTemplate = withInput ? '<input type="text" name="colorpicker-input" spellcheck="false">' : '',
278 closeButton = !inline ? '<button type="button" class="close close-colorpicker">×</button>' : '',
280 '<div class="colorpicker dropdown">' +
281 '<div class="dropdown-menu">' +
282 '<colorpicker-saturation><i></i></colorpicker-saturation>' +
283 '<colorpicker-hue><i></i></colorpicker-hue>' +
284 '<colorpicker-alpha><i></i></colorpicker-alpha>' +
285 '<colorpicker-preview></colorpicker-preview>' +
290 colorpickerTemplate = angular.element(template),
299 sliderHue = colorpickerTemplate.find('colorpicker-hue'),
300 sliderSaturation = colorpickerTemplate.find('colorpicker-saturation'),
301 colorpickerPreview = colorpickerTemplate.find('colorpicker-preview'),
302 pickerColorPointers = colorpickerTemplate.find('i');
304 $compile(colorpickerTemplate)($scope);
305 colorpickerTemplate.css('min-width', parseInt(componentSize) + 29 + 'px');
306 sliderSaturation.css({
307 'width' : componentSizePx,
308 'height' : componentSizePx
310 sliderHue.css('height', componentSizePx);
313 var pickerColorInput = colorpickerTemplate.find('input');
314 pickerColorInput.css('width', componentSizePx);
316 .on('mousedown', function(event) {
317 event.stopPropagation();
319 .on('keyup', function() {
320 var newColor = this.value;
322 if (ngModel && ngModel.$modelValue !== newColor) {
323 $scope.$apply(ngModel.$setViewValue(newColor));
329 function bindMouseEvents() {
330 $document.on('mousemove', mousemove);
331 $document.on('mouseup', mouseup);
334 if (thisFormat === 'rgba') {
335 colorpickerTemplate.addClass('alpha');
336 sliderAlpha = colorpickerTemplate.find('colorpicker-alpha');
337 sliderAlpha.css('height', componentSizePx);
339 .on('click', function(event) {
340 Slider.setAlpha(event, fixedPosition, componentSize);
343 .on('mousedown', function(event) {
344 Slider.setAlpha(event, fixedPosition, componentSize);
347 .on('mouseup', function(event){
348 emitEvent('colorpicker-selected-alpha');
353 .on('click', function(event) {
354 Slider.setHue(event, fixedPosition, componentSize);
357 .on('mousedown', function(event) {
358 Slider.setHue(event, fixedPosition, componentSize);
361 .on('mouseup', function(event){
362 emitEvent('colorpicker-selected-hue');
366 .on('click', function(event) {
367 Slider.setSaturation(event, fixedPosition, componentSize);
369 if (angular.isDefined(attrs.colorpickerCloseOnSelect)) {
370 hideColorpickerTemplate();
373 .on('mousedown', function(event) {
374 Slider.setSaturation(event, fixedPosition, componentSize);
377 .on('mouseup', function(event){
378 emitEvent('colorpicker-selected-saturation');
382 colorpickerTemplate.addClass('colorpicker-fixed-position');
385 colorpickerTemplate.addClass('colorpicker-position-' + position);
386 if (inline === 'true') {
387 colorpickerTemplate.addClass('colorpicker-inline');
390 target.append(colorpickerTemplate);
393 ngModel.$render = function () {
394 elem.val(ngModel.$viewValue);
400 elem.on('blur keyup change', function() {
404 elem.on('$destroy', function() {
405 colorpickerTemplate.remove();
408 function previewColor() {
410 colorpickerPreview.css('backgroundColor', pickerColor[thisFormat]());
412 colorpickerPreview.css('backgroundColor', pickerColor.toHex());
414 sliderSaturation.css('backgroundColor', pickerColor.toHex(pickerColor.value.h, 1, 1, 1));
415 if (thisFormat === 'rgba') {
416 sliderAlpha.css.backgroundColor = pickerColor.toHex();
420 function mousemove(event) {
422 left = Slider.getLeftPosition(event),
423 top = Slider.getTopPosition(event),
424 slider = Slider.getSlider();
426 Slider.setKnob(top, left);
428 if (slider.callLeft) {
429 pickerColor[slider.callLeft].call(pickerColor, left / componentSize);
431 if (slider.callTop) {
432 pickerColor[slider.callTop].call(pickerColor, top / componentSize);
435 var newColor = pickerColor[thisFormat]();
438 $scope.$apply(ngModel.$setViewValue(newColor));
441 pickerColorInput.val(newColor);
447 emitEvent('colorpicker-selected');
448 $document.off('mousemove', mousemove);
449 $document.off('mouseup', mouseup);
452 function update(omitInnerInput) {
453 pickerColor.value = colorpickerValue;
454 pickerColor.setColor(elem.val());
455 if (withInput && !omitInnerInput) {
456 pickerColorInput.val(elem.val());
458 pickerColorPointers.eq(0).css({
459 left: pickerColor.value.s * componentSize + 'px',
460 top: componentSize - pickerColor.value.b * componentSize + 'px'
462 pickerColorPointers.eq(1).css('top', componentSize * (1 - pickerColor.value.h) + 'px');
463 pickerColorPointers.eq(2).css('top', componentSize * (1 - pickerColor.value.a) + 'px');
464 colorpickerValue = pickerColor.value;
468 function getColorpickerTemplatePosition() {
471 positionOffset = Helper.getOffset(elem[0]);
473 if(angular.isDefined(attrs.colorpickerParent)) {
474 positionOffset.left = 0;
475 positionOffset.top = 0;
478 if (position === 'top') {
480 'top': positionOffset.top - 147,
481 'left': positionOffset.left
483 } else if (position === 'right') {
485 'top': positionOffset.top,
486 'left': positionOffset.left + 126
488 } else if (position === 'bottom') {
490 'top': positionOffset.top + elem[0].offsetHeight + 2,
491 'left': positionOffset.left
493 } else if (position === 'left') {
495 'top': positionOffset.top,
496 'left': positionOffset.left - 150
500 'top': positionValue.top + 'px',
501 'left': positionValue.left + 'px'
505 function documentMousedownHandler() {
506 hideColorpickerTemplate();
509 function showColorpickerTemplate() {
511 if (!colorpickerTemplate.hasClass('colorpicker-visible')) {
514 .addClass('colorpicker-visible')
515 .css(getColorpickerTemplatePosition());
516 emitEvent('colorpicker-shown');
518 if (inline === false) {
519 // register global mousedown event to hide the colorpicker
520 $document.on('mousedown', documentMousedownHandler);
523 if (attrs.colorpickerIsOpen) {
524 $scope[attrs.colorpickerIsOpen] = true;
525 if (!$scope.$$phase || !$scope.$root.$$phase) {
526 $scope.$digest(); //trigger the watcher to fire
532 if (inline === false) {
533 elem.on('click', showColorpickerTemplate);
535 showColorpickerTemplate();
538 colorpickerTemplate.on('mousedown', function (event) {
539 event.stopPropagation();
540 event.preventDefault();
543 function emitEvent(name) {
547 value: ngModel.$modelValue
552 function hideColorpickerTemplate() {
553 if (colorpickerTemplate.hasClass('colorpicker-visible')) {
554 colorpickerTemplate.removeClass('colorpicker-visible');
555 emitEvent('colorpicker-closed');
556 // unregister the global mousedown event
557 $document.off('mousedown', documentMousedownHandler);
559 if (attrs.colorpickerIsOpen) {
560 $scope[attrs.colorpickerIsOpen] = false;
561 if (!$scope.$$phase || !$scope.$root.$$phase) {
562 $scope.$digest(); //trigger the watcher to fire
568 colorpickerTemplate.find('button').on('click', function () {
569 hideColorpickerTemplate();
572 if (attrs.colorpickerIsOpen) {
573 $scope.$watch(attrs.colorpickerIsOpen, function(shouldBeOpen) {
575 if (shouldBeOpen === true) {
576 showColorpickerTemplate();
577 } else if (shouldBeOpen === false) {
578 hideColorpickerTemplate();