Skip to content

Instantly share code, notes, and snippets.

@clouddueling
Last active August 29, 2015 13:56
Show Gist options
  • Save clouddueling/9072760 to your computer and use it in GitHub Desktop.
Save clouddueling/9072760 to your computer and use it in GitHub Desktop.
'use strict';
angular.module('colorpicker.module', [])
.factory('Helper', function () {
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) {
var
x = 0,
y = 0,
scrollX = 0,
scrollY = 0;
while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
x += elem.offsetLeft;
y += elem.offsetTop;
scrollX += elem.scrollLeft;
scrollY += elem.scrollTop;
elem = elem.offsetParent;
}
return {
top: y,
left: x,
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) {
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.toLowerCase();
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),
space = parser.space || 'rgba';
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) {
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) {
var
target = Helper.closestSlider(event.target),
targetOffset = Helper.getOffset(target);
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,
top: event.pageY
};
},
setSaturation: function(event) {
slider = {
maxLeft: 100,
maxTop: 100,
callLeft: 'setSaturation',
callTop: 'setLightness'
};
this.setSlider(event)
},
setHue: function(event) {
slider = {
maxLeft: 0,
maxTop: 100,
callLeft: false,
callTop: 'setHue'
};
this.setSlider(event)
},
setAlpha: function(event) {
slider = {
maxLeft: 0,
maxTop: 100,
callLeft: false,
callTop: 'setAlpha'
};
this.setSlider(event)
},
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) {
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',
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,
inputTemplate = withInput ? '<input type="text" name="colorpicker-input">' : '',
template = angular.isDefined(attrs.colorpickerTemplate) ? attrs.colorpickerTemplate : 'default',
templates = {
default: '<div class="colorpicker dropdown">' +
'<div class="dropdown-menu">' +
'<colorpicker-saturation><i></i></colorpicker-saturation>' +
'<colorpicker-hue><i></i></colorpicker-hue>' +
'<colorpicker-alpha><i></i></colorpicker-alpha>' +
'<colorpicker-preview></colorpicker-preview>' +
inputTemplate +
'<button class="close close-colorpicker">&times;</button>' +
'</div>' +
'</div>',
inline: '<div class="colorpicker colorpicker-inline colorpicker-visible">' +
'<div class="dropdown-menu">' +
'<colorpicker-saturation><i></i></colorpicker-saturation>' +
'<colorpicker-hue><i></i></colorpicker-hue>' +
'<colorpicker-alpha><i></i></colorpicker-alpha>' +
'<colorpicker-preview></colorpicker-preview>' +
inputTemplate +
'</div>' +
'</div>'
},
colorpickerTemplate = angular.element(templates[template]),
pickerColor = Color,
sliderAlpha,
sliderHue = colorpickerTemplate.find('colorpicker-hue'),
sliderSaturation = colorpickerTemplate.find('colorpicker-saturation'),
colorpickerPreview = colorpickerTemplate.find('colorpicker-preview'),
pickerColorPointers = colorpickerTemplate.find('i');
$compile(colorpickerTemplate)($scope);
if (withInput) {
var pickerColorInput = colorpickerTemplate.find('input');
pickerColorInput
.on('mousedown', function() {
event.stopPropagation();
})
.on('keyup', function(event) {
var newColor = this.value;
elem.val(newColor);
if(ngModel) {
$scope.$apply(ngModel.$setViewValue(newColor));
}
event.stopPropagation();
event.preventDefault();
});
elem.on('keyup', function() {
pickerColorInput.val(elem.val());
});
}
var bindMouseEvents = function() {
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
};
if (thisFormat === 'rgba') {
colorpickerTemplate.addClass('alpha');
sliderAlpha = colorpickerTemplate.find('colorpicker-alpha');
sliderAlpha
.on('click', function(event) {
Slider.setAlpha(event);
mousemove(event);
})
.on('mousedown', function(event) {
Slider.setAlpha(event);
bindMouseEvents();
});
}
sliderHue
.on('click', function(event) {
Slider.setHue(event);
mousemove(event);
})
.on('mousedown', function(event) {
Slider.setHue(event);
bindMouseEvents();
});
sliderSaturation
.on('click', function(event) {
Slider.setSaturation(event);
mousemove(event);
})
.on('mousedown', function(event) {
Slider.setSaturation(event);
bindMouseEvents();
});
if (fixedPosition) {
colorpickerTemplate.addClass('colorpicker-fixed-position');
}
colorpickerTemplate.addClass('colorpicker-position-' + position);
target.append(colorpickerTemplate);
if(ngModel) {
ngModel.$render = function () {
elem.val(ngModel.$viewValue);
};
$scope.$watch(attrs.ngModel, function() {
update();
});
}
elem.on('$destroy', function() {
colorpickerTemplate.remove();
});
var previewColor = function () {
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();
}
};
var mousemove = function (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 / 100);
}
if (slider.callTop) {
pickerColor[slider.callTop].call(pickerColor, top / 100);
}
previewColor();
var newColor = pickerColor[thisFormat]();
elem.val(newColor);
if(ngModel) {
$scope.$apply(ngModel.$setViewValue(newColor));
}
if (withInput) {
pickerColorInput.val(newColor);
}
return false;
};
var mouseup = function () {
$document.off('mousemove', mousemove);
$document.off('mouseup', mouseup);
};
var update = function () {
if (withInput) {
pickerColorInput.val(elem.val());
}
pickerColor.setColor(elem.val());
pickerColorPointers.eq(0).css({
left: pickerColor.value.s * 100 + 'px',
top: 100 - pickerColor.value.b * 100 + 'px'
});
pickerColorPointers.eq(1).css('top', 100 * (1 - pickerColor.value.h) + 'px');
pickerColorPointers.eq(2).css('top', 100 * (1 - pickerColor.value.a) + 'px');
previewColor();
};
var getColorpickerTemplatePosition = function() {
var
positionValue,
positionOffset = Helper.getOffset(elem[0]);
if(angular.isDefined(attrs.colorpickerParent)) {
positionOffset.left = 0;
positionOffset.top = 0;
}
if (position === 'top') {
positionValue = {
'top': positionOffset.top - 147,
'left': positionOffset.left
};
} else if (position === 'right') {
positionValue = {
'top': positionOffset.top,
'left': positionOffset.left + 126
};
} else if (position === 'bottom') {
positionValue = {
'top': positionOffset.top + elem[0].offsetHeight + 2,
'left': positionOffset.left
};
} else if (position === 'left') {
positionValue = {
'top': positionOffset.top,
'left': positionOffset.left - 150
};
}
return {
'top': positionValue.top + 'px',
'left': positionValue.left + 'px'
};
};
elem.on('click', function () {
update();
if (template !== 'inline') {
colorpickerTemplate
.addClass('colorpicker-visible')
.css(getColorpickerTemplatePosition());
}
});
colorpickerTemplate.on('mousedown', function (event) {
event.stopPropagation();
event.preventDefault();
});
var hideColorpickerTemplate = function() {
if (colorpickerTemplate.hasClass('colorpicker-visible')) {
colorpickerTemplate.removeClass('colorpicker-visible');
}
};
colorpickerTemplate.find('button').on('click', function () {
hideColorpickerTemplate();
});
$document.on('mousedown', function () {
if (template !== 'inline') {
hideColorpickerTemplate();
}
});
update();
}
};
}]);
.colorpicker-visible,
.colorpicker-visible .dropdown-menu {
display: block !important;
}
colorpicker-saturation {
display: block;
width: 100px;
height: 100px;
background-image: data-uri('../img/saturation.png');
cursor: crosshair;
float: left;
i {
display: block;
height: 7px;
width: 7px;
border: 1px solid #000;
border-radius: 5px;
position: absolute;
top: 0;
left: 0;
margin: -4px 0 0 -4px;
&::after {
content: '';
display: block;
height: 7px;
width: 7px;
border: 1px solid #fff;
border-radius: 5px;
}
}
}
colorpicker-hue,
colorpicker-alpha {
width: 15px;
height: 100px;
float: left;
cursor: row-resize;
margin-left: 4px;
margin-bottom: 4px;
i {
display: block;
height: 2px;
background: #000;
border-top: 1px solid #fff;
position: absolute;
top: 0;
left: 0;
width: 100%;
margin-top: -1px;
}
}
colorpicker-hue {
background-image: data-uri('../img/hue.png');
}
colorpicker-alpha {
display: none;
}
colorpicker-alpha,
.colorpicker-color {
background-image: data-uri('../img/alpha.png');
}
.colorpicker {
top: 0;
left: 0;
position: absolute;
z-index: 9999;
display: none;
colorpicker-hue,
colorpicker-alpha,
colorpicker-saturation {
position: relative;
}
input {
width: 100px;
font-size: 11px;
color: #000;
background-color: #fff;
}
&.alpha {
min-width: 140px;
colorpicker-alpha {
display: block;
}
}
&.colorpicker-fixed-position {
position: fixed;
}
.dropdown-menu {
&::after,
&::before {
content: '';
display: inline-block;
position: absolute;
}
&::after {
clear: both;
border: 6px solid transparent;
top: -5px;
left: 7px;
}
&::before {
border: 7px solid transparent;
top: -6px;
left: 6px;
}
}
.dropdown-menu {
position: static;
top: 0;
left: 0;
min-width: 120px;
padding: 4px;
margin-top: 0;
}
}
.colorpicker-position-top {
.dropdown-menu {
&::after {
border-top: 6px solid #fff;
border-bottom: 0;
top: auto;
bottom: -5px;
}
&::before {
border-top: 7px solid rgba(0, 0, 0, 0.2);
border-bottom: 0;
top: auto;
bottom: -6px;
}
}
}
.colorpicker-position-right {
.dropdown-menu {
&::after {
border-right: 6px solid #fff;
border-left: 0;
top: 11px;
left: -5px;
}
&::before {
border-right: 7px solid rgba(0, 0, 0, 0.2);
border-left: 0;
top: 10px;
left: -6px;
}
}
}
.colorpicker-position-bottom {
.dropdown-menu {
&::after {
border-bottom: 6px solid #fff;
border-top: 0;
}
&::before {
border-bottom: 7px solid rgba(0, 0, 0, 0.2);
border-top: 0;
}
}
}
.colorpicker-position-left {
.dropdown-menu {
&::after {
border-left: 6px solid #fff;
border-right: 0;
top: 11px;
left: auto;
right: -5px;
}
&::before {
border-left: 7px solid rgba(0, 0, 0, 0.2);
border-right: 0;
top: 10px;
left: auto;
right: -6px;
}
}
}
colorpicker-preview {
display: block;
height: 10px;
margin: 5px 0 3px 0;
clear: both;
background-position: 0 100%;
}
.colorpicker-inline {
position: relative;
.dropdown-menu:after {
border: none !important;
content: "" !important;
}
.dropdown-menu:before {
border: none !important;
content: "" !important;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment