Last active
August 29, 2015 13:56
-
-
Save clouddueling/9072760 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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">×</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(); | |
} | |
}; | |
}]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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