diff --git a/index.html b/index.html new file mode 100644 index 0000000..829e70f --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/materialcolorpicker.js b/materialcolorpicker.js new file mode 100644 index 0000000..a4b2fdd --- /dev/null +++ b/materialcolorpicker.js @@ -0,0 +1,304 @@ + +/** + * A javascript color picker inspired by material design color choice + * @requires: jQuery, data-attribute + * @author https://github.com/masbaehr + * @constructor + */ +function MaterialColorPickerJS(inputElement) { + + //CSS styles - avoid creation of a separate file for this minor css, so we can modify everything related to the picker in this file + var containerStyle = "display: none; text-align: left; position: absolute; background: #444444; border: 1px solid #444444; z-index: 9999; max-width: 250px; padding-left: 2px; font-family: Lucida Console; padding-top: 2px;"; + var colorStyle = "cursor: pointer; display: inline-block; width: 6.6%; line-height: 100%; box-sizing: border-box;"; + + var _this = this; + this.inputElement = inputElement; + //create the container if not initialized + if($(inputElement).data("init") === true){ + return; + } + //create a containerelement + $("
").insertAfter(inputElement); + var currentContainer = $(inputElement).next(); + //build the color map inspired by: https://material.io/guidelines/style/color.html + var materialIoCols = ['#F44336', '#FFEBEE', '#FFCDD2', '#EF9A9A', '#E57373', '#EF5350', '#F44336', '#E53935', '#D32F2F', '#C62828', '#B71C1C', '#FF8A80', '#FF5252', '#FF1744', '#D50000', '#E91E63', '#FCE4EC', '#F8BBD0', '#F48FB1', '#F06292', '#EC407A', '#E91E63', '#D81B60', '#C2185B', '#AD1457', '#880E4F', '#FF80AB', '#FF4081', '#F50057', '#C51162', '#9C27B0', '#F3E5F5', '#E1BEE7', '#CE93D8', '#BA68C8', '#AB47BC', '#9C27B0', '#8E24AA', '#7B1FA2', '#6A1B9A', '#4A148C', '#EA80FC', '#E040FB', '#D500F9', '#AA00FF', '#673AB7', '#EDE7F6', '#D1C4E9', '#B39DDB', '#9575CD', '#7E57C2', '#673AB7', '#5E35B1', '#512DA8', '#4527A0', '#311B92', '#B388FF', '#7C4DFF', '#651FFF', '#6200EA', '#3F51B5', '#E8EAF6', '#C5CAE9', '#9FA8DA', '#7986CB', '#5C6BC0', '#3F51B5', '#3949AB', '#303F9F', '#283593', '#1A237E', '#8C9EFF', '#536DFE', '#3D5AFE', '#304FFE', '#2196F3', '#E3F2FD', '#BBDEFB', '#90CAF9', '#64B5F6', '#42A5F5', '#2196F3', '#1E88E5', '#1976D2', '#1565C0', '#0D47A1', '#82B1FF', '#448AFF', '#2979FF', '#2962FF', '#03A9F4', '#E1F5FE', '#B3E5FC', '#81D4FA', '#4FC3F7', '#29B6F6', '#03A9F4', '#039BE5', '#0288D1', '#0277BD', '#01579B', '#80D8FF', '#40C4FF', '#00B0FF', '#0091EA', '#00BCD4', '#E0F7FA', '#B2EBF2', '#80DEEA', '#4DD0E1', '#26C6DA', '#00BCD4', '#00ACC1', '#0097A7', '#00838F', '#006064', '#84FFFF', '#18FFFF', '#00E5FF', '#00B8D4', '#009688', '#E0F2F1', '#B2DFDB', '#80CBC4', '#4DB6AC', '#26A69A', '#009688', '#00897B', '#00796B', '#00695C', '#004D40', '#A7FFEB', '#64FFDA', '#1DE9B6', '#00BFA5', '#4CAF50', '#E8F5E9', '#C8E6C9', '#A5D6A7', '#81C784', '#66BB6A', '#4CAF50', '#43A047', '#388E3C', '#2E7D32', '#1B5E20', '#B9F6CA', '#69F0AE', '#00E676', '#00C853', '#8BC34A', '#F1F8E9', '#DCEDC8', '#C5E1A5', '#AED581', '#9CCC65', '#8BC34A', '#7CB342', '#689F38', '#558B2F', '#33691E', '#CCFF90', '#B2FF59', '#76FF03', '#64DD17', '#CDDC39', '#F9FBE7', '#F0F4C3', '#E6EE9C', '#DCE775', '#D4E157', '#CDDC39', '#C0CA33', '#AFB42B', '#9E9D24', '#827717', '#F4FF81', '#EEFF41', '#C6FF00', '#AEEA00', '#FFEB3B', '#FFFDE7', '#FFF9C4', '#FFF59D', '#FFF176', '#FFEE58', '#FFEB3B', '#FDD835', '#FBC02D', '#F9A825', '#F57F17', '#FFFF8D', '#FFFF00', '#FFEA00', '#FFD600', '#FFC107', '#FFF8E1', '#FFECB3', '#FFE082', '#FFD54F', '#FFCA28', '#FFC107', '#FFB300', '#FFA000', '#FF8F00', '#FF6F00', '#FFE57F', '#FFD740', '#FFC400', '#FFAB00', '#FF9800', '#FFF3E0', '#FFE0B2', '#FFCC80', '#FFB74D', '#FFA726', '#FF9800', '#FB8C00', '#F57C00', '#EF6C00', '#E65100', '#FFD180', '#FFAB40', '#FF9100', '#FF6D00', '#FF5722', '#FBE9E7', '#FFCCBC', '#FFAB91', '#FF8A65', '#FF7043', '#FF5722', '#F4511E', '#E64A19', '#D84315', '#BF360C', '#FF9E80', '#FF6E40', '#FF3D00', '#DD2C00', '#795548', '#EFEBE9', '#D7CCC8', '#BCAAA4', '#A1887F', '#8D6E63', '#795548', '#6D4C41', '#5D4037', '#4E342E', '#3E2723', '#9E9E9E', '#FAFAFA', '#F5F5F5', '#EEEEEE', '#E0E0E0', '#BDBDBD', '#9E9E9E', '#757575', '#616161', '#424242', '#212121', '#607D8B', '#ECEFF1', '#CFD8DC', '#B0BEC5', '#90A4AE', '#78909C', '#607D8B', '#546E7A', '#455A64', '#37474F', '#263238', '#000000', '#FFFFFF']; + $(materialIoCols).each(function(index, el) { + currentContainer.append("
 
"); + currentContainer.children().last().on("click", function(event){ + inputElement.value = $(event.currentTarget).data("color"); + $(inputElement).attr("value", $(event.currentTarget).data("color")); + $(inputElement).css("background", $(event.currentTarget).data("color")); + $(inputElement).css("color", _this.invertColor($(event.currentTarget).data("color"), true)); + $(inputElement).trigger("change"); + $(currentContainer).fadeOut(); + _this.fillHSBInput(inputElement.value); + _this.fillRGBInput(inputElement.value); + }); + }); + + var validateRGBHSVInput = function(event){ + var isNumber = (event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >= 96 && event.keyCode <= 105); + var isDot = event.keyCode === 190; + var isControl = event.keyCode === 8 || (event.keyCode >= 37 && event.keyCode <= 40) || event.keyCode === 46 || event.keyCode === 9 || event.keyCode === 17; + if(event.keyCode === 13) { + $(currentContainer).fadeOut(); + event.preventDefault(); + return false; + } + if (!isNumber && !isControl && !isDot) { + event.preventDefault(); + } + }; + + currentContainer.append("
"); + var manualInputTemplate = "
#
"; + + //HSV-Input + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "H")); + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "S")); + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "V")); + currentContainer.find("input").on("keydown", validateRGBHSVInput); + currentContainer.find("input").on("keyup", function(event){ + _this.validateHSB(event.currentTarget); + }); + currentContainer.find("input").on("blur", function(event){ + _this.commitHSV(); + if($(currentContainer).data("bluractive")){ + $(currentContainer).fadeOut(); + } + }); + //RGB-Input + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "R")); + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "G")); + currentContainer.append(_this.replaceAll(manualInputTemplate, "#", "B")); + currentContainer.find("input").on("keydown", validateRGBHSVInput); + currentContainer.find("input").on("keyup", function(event){ + _this.validateRGB(event.currentTarget); + }); + currentContainer.find("input").on("blur", function(event){ + _this.commitRGB(); + if($(currentContainer).data("bluractive")){ + $(currentContainer).fadeOut(); + } + }); + + //set the initial color + $(inputElement).css("background", $(inputElement).val()); + $(inputElement).css("color", _this.invertColor($(inputElement).val(), true)); + $(inputElement).css("width", "80px"); + $(inputElement).css("text-transform", "uppercase"); + //track if mouse cursor is on the picker to decide if the blur event is active or not + $(currentContainer).data("bluractive", true); + $(currentContainer).on("mouseenter", function(e){ + $(currentContainer).data("bluractive", false); + }); + $(currentContainer).on("mouseleave", function(e){ + $(currentContainer).data("bluractive", true); + }); + //set the js event handler for focus and blur on the input element of the picker + $(inputElement).on("focus", function(e){ + $(currentContainer).fadeIn(); + _this.fillHSBInput(inputElement.value); + _this.fillRGBInput(inputElement.value); + }); + $(inputElement).on("blur", function(e){ + if($(currentContainer).data("bluractive")){ + $(currentContainer).fadeOut(); + } + }); + $(inputElement).on("keyup", function(e){ + var isOk = /^#[0-9A-F]{6}$/i.test(inputElement.value); + if(isOk){ + $(inputElement).css("background", inputElement.value); + $(inputElement).css("color", _this.invertColor(inputElement.value, true)); + } + }); + + this.fillHSBInput(inputElement.value); + this.fillRGBInput(inputElement.value); + + //set init flag, so the picker will not be initialized again after ajax update + $(inputElement).data("init", true); +} + +MaterialColorPickerJS.prototype.replaceAll = function(target, search, replacement) { + return target.replace(new RegExp(search, 'g'), replacement); +}; + +MaterialColorPickerJS.prototype.validateHSB = function(target) { + var container = $(this.inputElement).next(); + var hsbtype = $(target).data("composite"); + var value = $(target).val(); + var invalid = false; + if(hsbtype === 'H' && (isNaN(value) || value < 0 || value > 360)){ + $(target).parent().css("border", "1px solid red"); + invalid = true; + } + if((hsbtype === 'S' || hsbtype === 'V') && (isNaN(value) || value < 0 || value > 100)){ + $(target).parent().css("border", "1px solid red"); + invalid = true; + } + if(!invalid){ + $(target).parent().css("border", "1px solid gray"); + } +}; +MaterialColorPickerJS.prototype.validateRGB = function(target) { + var container = $(this.inputElement).next(); + var value = $(target).val(); + if(isNaN(value) || value < 0 || value > 255){ + $(target).parent().css("border", "1px solid red"); + } else { + $(target).parent().css("border", "1px solid gray"); + } + //TODO: Do not just colorize the border, instead do not accept invalids +}; + +MaterialColorPickerJS.prototype.commitRGB = function() { + var container = $(this.inputElement).next(); + var input_r = container.find("input[data-composite='R']").val(); + var input_g = container.find("input[data-composite='G']").val(); + var input_b = container.find("input[data-composite='B']").val(); + var hexColor = this.rgbToHex(parseInt(input_r), parseInt(input_g), parseInt(input_b)); + this.commitHEX(hexColor); +}; +MaterialColorPickerJS.prototype.commitHSV = function() { + var container = $(this.inputElement).next(); + var input_H = container.find("input[data-composite='H']").val(); + var input_S = container.find("input[data-composite='S']").val(); + var input_V = container.find("input[data-composite='V']").val(); + var rgbCol = this.hsvToRgb(input_H, input_S, input_V, true); + this.commitHEX(this.rgbToHex(rgbCol.r, rgbCol.g, rgbCol.b)); +}; +MaterialColorPickerJS.prototype.commitHEX = function(hexColor) { + if (hexColor.length !== 7) { + throw new Error('Invalid #RRGGBB color.'); + } + this.inputElement.value = hexColor; + $(this.inputElement).attr("value", hexColor); + $(this.inputElement).css("background", hexColor); + $(this.inputElement).css("color", this.invertColor(hexColor, true)); + $(this.inputElement).trigger("change"); + this.fillHSBInput(hexColor); + this.fillRGBInput(hexColor); +}; + +MaterialColorPickerJS.prototype.componentToHex = function(c) { + var hex = c.toString(16); + return hex.length === 1 ? "0" + hex : hex; +}; +MaterialColorPickerJS.prototype.hsvToRgb = function(h, s, v, degrees) { + //use degrees + if(degrees) { + h = h / 360; + s = s / 100; + v = v / 100; + } + //remove this line to use values 0.0 - 1.0 + var r, g, b, i, f, p, q, t; + if (arguments.length === 1) { + s = h.s, v = h.v, h = h.h; + } + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +}; +MaterialColorPickerJS.prototype.rgbToHex = function(r, g, b) { + return "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b); +}; + +MaterialColorPickerJS.prototype.rgbToHsv = function(r, g, b, degrees) { + r /= 255; g /= 255; b /= 255; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, v = max; + var d = max - min; + s = max === 0 ? 0 : d / max; + if (max === min) { + h = 0; // achromatic + } else { + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + if(degrees === true){ + return [ parseFloat(h*360).toFixed(2), parseFloat(s*100).toFixed(2), parseFloat(v*100).toFixed(2) ]; + } else{ + return [ parseFloat(h).toFixed(2), parseFloat(s).toFixed(2), parseFloat(v).toFixed(2) ]; + } +}; + +MaterialColorPickerJS.prototype.hexToRgb = function(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +MaterialColorPickerJS.prototype.fillHSBInput = function(hexColor) { + var container = $(this.inputElement).next(); + var rgbv = this.hexToRgb(hexColor); + var hsv = this.rgbToHsv(rgbv.r, rgbv.g, rgbv.b, true); + var input_r = container.find("input[data-composite='H']").val(hsv[0]); + var input_g = container.find("input[data-composite='S']").val(hsv[1]); + var input_b = container.find("input[data-composite='V']").val(hsv[2]); +}; +MaterialColorPickerJS.prototype.fillRGBInput = function(hexColor) { + var container = $(this.inputElement).next(); + var rgbv = this.hexToRgb(hexColor); + var input_r = container.find("input[data-composite='R']").val(rgbv.r); + var input_g = container.find("input[data-composite='G']").val(rgbv.g); + var input_b = container.find("input[data-composite='B']").val(rgbv.b); +}; + +/* More about this here: https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color */ +MaterialColorPickerJS.prototype.invertColor = function(hex, bw) { + if (hex.indexOf('#') === 0) { + hex = hex.slice(1); + } + // convert 3-digit hex to 6-digits. + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + if (hex.length !== 6) { + throw new Error('Invalid HEX color.'); + } + var r = parseInt(hex.slice(0, 2), 16), + g = parseInt(hex.slice(2, 4), 16), + b = parseInt(hex.slice(4, 6), 16); + if (bw) { + // http://stackoverflow.com/a/3943023/112731 + return (r * 0.299 + g * 0.587 + b * 0.114) > 186 + ? '#000000' + : '#FFFFFF'; + } + // invert color components + r = (255 - r).toString(16); + g = (255 - g).toString(16); + b = (255 - b).toString(16); + // pad each with zeros and return + return "#" + this.padZero(r) + this.padZero(g) + this.padZero(b); +}; +MaterialColorPickerJS.prototype.padZero = function(str, len) { + len = len || 2; + var zeros = new Array(len).join('0'); + return (zeros + str).slice(-len); +}; \ No newline at end of file