angular.module('colorpicker.module', []) .factory('Helper', function () { 'use strict'; return { closestSlider: function (elem) { var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector; if (matchesSelector.bind(elem)('I')) { return elem.parentNode; } return elem; }, getOffset: function (elem, fixedPosition) { var scrollX = 0, scrollY = 0, rect = elem.getBoundingClientRect(); while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) { if (!fixedPosition && elem.tagName === 'BODY') { scrollX += document.documentElement.scrollLeft || elem.scrollLeft; scrollY += document.documentElement.scrollTop || elem.scrollTop; } else { scrollX += elem.scrollLeft; scrollY += elem.scrollTop; } elem = elem.offsetParent; } return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset, scrollX: scrollX, scrollY: scrollY }; }, // a set of RE's that can match strings and generate color tuples. https://github.com/jquery/jquery-color/ stringParsers: [ { re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/, parse: function (execResult) { return [ execResult[1], execResult[2], execResult[3], execResult[4] ]; } }, { re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/, parse: function (execResult) { return [ 2.55 * execResult[1], 2.55 * execResult[2], 2.55 * execResult[3], execResult[4] ]; } }, { re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, parse: function (execResult) { return [ parseInt(execResult[1], 16), parseInt(execResult[2], 16), parseInt(execResult[3], 16) ]; } }, { re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/, parse: function (execResult) { return [ parseInt(execResult[1] + execResult[1], 16), parseInt(execResult[2] + execResult[2], 16), parseInt(execResult[3] + execResult[3], 16) ]; } } ] }; }) .factory('Color', ['Helper', function (Helper) { 'use strict'; return { value: { h: 1, s: 1, b: 1, a: 1 }, // translate a format from Color object to a string 'rgb': function () { var rgb = this.toRGB(); return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; }, 'rgba': function () { var rgb = this.toRGB(); return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + rgb.a + ')'; }, 'hex': function () { return this.toHex(); }, // HSBtoRGB from RaphaelJS RGBtoHSB: function (r, g, b, a) { r /= 255; g /= 255; b /= 255; var H, S, V, C; V = Math.max(r, g, b); C = V - Math.min(r, g, b); H = (C === 0 ? null : V === r ? (g - b) / C : V === g ? (b - r) / C + 2 : (r - g) / C + 4 ); H = ((H + 360) % 6) * 60 / 360; S = C === 0 ? 0 : C / V; return {h: H || 1, s: S, b: V, a: a || 1}; }, //parse a string to HSB setColor: function (val) { val = (val) ? val.toLowerCase() : val; for (var key in Helper.stringParsers) { if (Helper.stringParsers.hasOwnProperty(key)) { var parser = Helper.stringParsers[key]; var match = parser.re.exec(val), values = match && parser.parse(match); if (values) { this.value = this.RGBtoHSB.apply(null, values); return false; } } } }, setHue: function (h) { this.value.h = 1 - h; }, setSaturation: function (s) { this.value.s = s; }, setLightness: function (b) { this.value.b = 1 - b; }, setAlpha: function (a) { this.value.a = parseInt((1 - a) * 100, 10) / 100; }, // HSBtoRGB from RaphaelJS // https://github.com/DmitryBaranovskiy/raphael/ toRGB: function (h, s, b, a) { if (!h) { h = this.value.h; s = this.value.s; b = this.value.b; } h *= 360; var R, G, B, X, C; h = (h % 360) / 60; C = b * s; X = C * (1 - Math.abs(h % 2 - 1)); R = G = B = b - C; h = ~~h; R += [C, X, 0, 0, X, C][h]; G += [X, C, C, X, 0, 0][h]; B += [0, 0, X, C, C, X][h]; return { r: Math.round(R * 255), g: Math.round(G * 255), b: Math.round(B * 255), a: a || this.value.a }; }, toHex: function (h, s, b, a) { var rgb = this.toRGB(h, s, b, a); return '#' + ((1 << 24) | (parseInt(rgb.r, 10) << 16) | (parseInt(rgb.g, 10) << 8) | parseInt(rgb.b, 10)).toString(16).substr(1); } }; }]) .factory('Slider', ['Helper', function (Helper) { 'use strict'; var slider = { maxLeft: 0, maxTop: 0, callLeft: null, callTop: null, knob: { top: 0, left: 0 } }, pointer = {}; return { getSlider: function() { return slider; }, getLeftPosition: function(event) { return Math.max(0, Math.min(slider.maxLeft, slider.left + ((event.pageX || pointer.left) - pointer.left))); }, getTopPosition: function(event) { return Math.max(0, Math.min(slider.maxTop, slider.top + ((event.pageY || pointer.top) - pointer.top))); }, setSlider: function (event, fixedPosition) { var target = Helper.closestSlider(event.target), targetOffset = Helper.getOffset(target, fixedPosition), rect = target.getBoundingClientRect(), offsetX = event.clientX - rect.left, offsetY = event.clientY - rect.top; slider.knob = target.children[0].style; slider.left = event.pageX - targetOffset.left - window.pageXOffset + targetOffset.scrollX; slider.top = event.pageY - targetOffset.top - window.pageYOffset + targetOffset.scrollY; pointer = { left: event.pageX - (offsetX - slider.left), top: event.pageY - (offsetY - slider.top) }; }, setSaturation: function(event, fixedPosition, componentSize) { slider = { maxLeft: componentSize, maxTop: componentSize, callLeft: 'setSaturation', callTop: 'setLightness' }; this.setSlider(event, fixedPosition); }, setHue: function(event, fixedPosition, componentSize) { slider = { maxLeft: 0, maxTop: componentSize, callLeft: false, callTop: 'setHue' }; this.setSlider(event, fixedPosition); }, setAlpha: function(event, fixedPosition, componentSize) { slider = { maxLeft: 0, maxTop: componentSize, callLeft: false, callTop: 'setAlpha' }; this.setSlider(event, fixedPosition); }, setKnob: function(top, left) { slider.knob.top = top + 'px'; slider.knob.left = left + 'px'; } }; }]) .directive('colorpicker', ['$document', '$compile', 'Color', 'Slider', 'Helper', function ($document, $compile, Color, Slider, Helper) { 'use strict'; return { require: '?ngModel', restrict: 'A', link: function ($scope, elem, attrs, ngModel) { var thisFormat = attrs.colorpicker ? attrs.colorpicker : 'hex', position = angular.isDefined(attrs.colorpickerPosition) ? attrs.colorpickerPosition : 'bottom', inline = angular.isDefined(attrs.colorpickerInline) ? attrs.colorpickerInline : false, fixedPosition = angular.isDefined(attrs.colorpickerFixedPosition) ? attrs.colorpickerFixedPosition : false, target = angular.isDefined(attrs.colorpickerParent) ? elem.parent() : angular.element(document.body), withInput = angular.isDefined(attrs.colorpickerWithInput) ? attrs.colorpickerWithInput : false, componentSize = angular.isDefined(attrs.colorpickerSize) ? attrs.colorpickerSize : 100, componentSizePx = componentSize + 'px', inputTemplate = withInput ? '' : '', closeButton = !inline ? '' : '', template = '', colorpickerTemplate = angular.element(template), pickerColor = Color, colorpickerValue = { h: 1, s: 0, b: 1, a: 1 }, sliderAlpha, sliderHue = colorpickerTemplate.find('colorpicker-hue'), sliderSaturation = colorpickerTemplate.find('colorpicker-saturation'), colorpickerPreview = colorpickerTemplate.find('colorpicker-preview'), pickerColorPointers = colorpickerTemplate.find('i'), componentWidthWithToolbars = parseInt(componentSize) + 29 + (thisFormat === 'rgba' ? 15 : 0), componentHeightWithToolbars = parseInt(componentSize) + 55; $compile(colorpickerTemplate)($scope); colorpickerTemplate.css('min-width', componentWidthWithToolbars + 'px'); sliderSaturation.css({ 'width' : componentSizePx, 'height' : componentSizePx }); sliderHue.css('height', componentSizePx); if (withInput) { var pickerColorInput = colorpickerTemplate.find('input'); pickerColorInput.css('width', componentSizePx); pickerColorInput .on('mousedown', function(event) { event.stopPropagation(); }) .on('keyup', function() { var newColor = this.value; elem.val(newColor); if (ngModel && ngModel.$modelValue !== newColor) { $scope.$apply(ngModel.$setViewValue(newColor)); update(true); } }); } function bindMouseEvents() { $document.on('mousemove', mousemove); $document.on('mouseup', mouseup); } if (thisFormat === 'rgba') { colorpickerTemplate.addClass('alpha'); sliderAlpha = colorpickerTemplate.find('colorpicker-alpha'); sliderAlpha.css('height', componentSizePx); sliderAlpha .on('click', function(event) { Slider.setAlpha(event, fixedPosition, componentSize); mousemove(event); }) .on('mousedown', function(event) { Slider.setAlpha(event, fixedPosition, componentSize); bindMouseEvents(); }) .on('mouseup', function(event){ emitEvent('colorpicker-selected-alpha'); }); } sliderHue .on('click', function(event) { Slider.setHue(event, fixedPosition, componentSize); mousemove(event); }) .on('mousedown', function(event) { Slider.setHue(event, fixedPosition, componentSize); bindMouseEvents(); }) .on('mouseup', function(event){ emitEvent('colorpicker-selected-hue'); }); sliderSaturation .on('click', function(event) { Slider.setSaturation(event, fixedPosition, componentSize); mousemove(event); if (angular.isDefined(attrs.colorpickerCloseOnSelect)) { hideColorpickerTemplate(); } }) .on('mousedown', function(event) { Slider.setSaturation(event, fixedPosition, componentSize); bindMouseEvents(); }) .on('mouseup', function(event){ emitEvent('colorpicker-selected-saturation'); }); if (fixedPosition) { colorpickerTemplate.addClass('colorpicker-fixed-position'); } colorpickerTemplate.addClass('colorpicker-position-' + position); if (inline === 'true') { colorpickerTemplate.addClass('colorpicker-inline'); } target.append(colorpickerTemplate); if (ngModel) { ngModel.$render = function () { elem.val(ngModel.$viewValue); update(); }; } elem.on('blur keyup change', function() { update(); }); elem.on('$destroy', function() { colorpickerTemplate.remove(); }); function previewColor() { try { colorpickerPreview.css('backgroundColor', pickerColor[thisFormat]()); } catch (e) { colorpickerPreview.css('backgroundColor', pickerColor.toHex()); } sliderSaturation.css('backgroundColor', pickerColor.toHex(pickerColor.value.h, 1, 1, 1)); if (thisFormat === 'rgba') { sliderAlpha.css.backgroundColor = pickerColor.toHex(); } } function mousemove(event) { var left = Slider.getLeftPosition(event), top = Slider.getTopPosition(event), slider = Slider.getSlider(); Slider.setKnob(top, left); if (slider.callLeft) { pickerColor[slider.callLeft].call(pickerColor, left / componentSize); } if (slider.callTop) { pickerColor[slider.callTop].call(pickerColor, top / componentSize); } previewColor(); var newColor = pickerColor[thisFormat](); elem.val(newColor); if (ngModel) { $scope.$apply(ngModel.$setViewValue(newColor)); } if (withInput) { pickerColorInput.val(newColor); } return false; } function mouseup() { emitEvent('colorpicker-selected'); $document.off('mousemove', mousemove); $document.off('mouseup', mouseup); } function update(omitInnerInput) { pickerColor.value = colorpickerValue; pickerColor.setColor(elem.val()); if (withInput && !omitInnerInput) { pickerColorInput.val(elem.val()); } pickerColorPointers.eq(0).css({ left: pickerColor.value.s * componentSize + 'px', top: componentSize - pickerColor.value.b * componentSize + 'px' }); pickerColorPointers.eq(1).css('top', componentSize * (1 - pickerColor.value.h) + 'px'); pickerColorPointers.eq(2).css('top', componentSize * (1 - pickerColor.value.a) + 'px'); colorpickerValue = pickerColor.value; previewColor(); } function getColorpickerTemplatePosition() { var positionValue, positionOffset = Helper.getOffset(elem[0]), additionalSpaceBetweenElements = 2; if(angular.isDefined(attrs.colorpickerParent)) { positionOffset.left = 0; positionOffset.top = 0; } if (position === 'top') { positionValue = { 'top': positionOffset.top - componentHeightWithToolbars - additionalSpaceBetweenElements, 'left': positionOffset.left }; } else if (position === 'right') { positionValue = { 'top': positionOffset.top, 'left': positionOffset.left + elem[0].offsetWidth + additionalSpaceBetweenElements }; } else if (position === 'bottom') { positionValue = { 'top': positionOffset.top + elem[0].offsetHeight + additionalSpaceBetweenElements, 'left': positionOffset.left }; } else if (position === 'left') { positionValue = { 'top': positionOffset.top, 'left': positionOffset.left - componentWidthWithToolbars - additionalSpaceBetweenElements }; } return { 'top': positionValue.top + 'px', 'left': positionValue.left + 'px' }; } function documentMousedownHandler() { hideColorpickerTemplate(); } function showColorpickerTemplate() { if (!colorpickerTemplate.hasClass('colorpicker-visible')) { update(); colorpickerTemplate .addClass('colorpicker-visible') .css(getColorpickerTemplatePosition()); emitEvent('colorpicker-shown'); if (inline === false) { // register global mousedown event to hide the colorpicker $document.on('mousedown', documentMousedownHandler); } if (attrs.colorpickerIsOpen) { $scope[attrs.colorpickerIsOpen] = true; if (!$scope.$$phase || !$scope.$root.$$phase) { $scope.$digest(); //trigger the watcher to fire } } } } if (inline === false) { elem.on('click', showColorpickerTemplate); } else { showColorpickerTemplate(); } colorpickerTemplate.on('mousedown', function (event) { event.stopPropagation(); event.preventDefault(); }); function emitEvent(name) { if (ngModel) { $scope.$emit(name, { name: attrs.ngModel, value: ngModel.$modelValue }); } } function hideColorpickerTemplate() { if (colorpickerTemplate.hasClass('colorpicker-visible')) { colorpickerTemplate.removeClass('colorpicker-visible'); emitEvent('colorpicker-closed'); // unregister the global mousedown event $document.off('mousedown', documentMousedownHandler); if (attrs.colorpickerIsOpen) { $scope[attrs.colorpickerIsOpen] = false; if (!$scope.$$phase || !$scope.$root.$$phase) { $scope.$digest(); //trigger the watcher to fire } } } } colorpickerTemplate.find('button').on('click', function () { hideColorpickerTemplate(); }); if (attrs.colorpickerIsOpen) { $scope.$watch(attrs.colorpickerIsOpen, function(shouldBeOpen) { if (shouldBeOpen === true) { showColorpickerTemplate(); } else if (shouldBeOpen === false) { hideColorpickerTemplate(); } }); } } }; }]);