UNPKG

jquery-wheelcolorpicker

Version:

The Wheel Color Picker plugin adds color picker functionality to HTML form inputs in round color wheel style. The Wheel Color Picker can be displayed as a popup dialog as users focus the input, or embedded inline.

1,570 lines (1,392 loc) 80.1 kB
/** * Wheel Color Picker for jQuery * * https://raffer.one/projects/jquery-wheelcolorpicker * * Author : Fajar Chandra * Date : 2019.11.05 * * Copyright © 2011-2019 Fajar Chandra. All rights reserved. * Released under MIT License. * http://www.opensource.org/licenses/mit-license.php */ (function ($) { /** * Function: wheelColorPicker * * The wheelColorPicker plugin wrapper. Firing all functions and * setting/getting all options in this plugin should be called via * this function. * * Before that, if wheelColorPicker instance is not yet initialized, * this will initialize ColorPicker widget. * * This function will look for the options parameter passed in, and * try to do something as specified in this order: * 1. If no argument is passed, then initialize the plugin or do nothing * 2. If object is passed, then call setOptions() * 3. If string is passed, then try to fire a method with that name * 4. If string is passed and no method matches the name, then try * to set/get an option with that name. If a setter/getter method * available (i.e. setSomething), it will set/get that option via that method. */ $.fn.wheelColorPicker = function() { var returnValue = this; // Allows method chaining // Separate first argument and the rest.. // First argument is used to determine function/option name // e.g. wheelColorPicker('setColor', { r: 0, g: 0, b: 1 }) if(arguments.length > 0) { var shift = [].shift; var firstArg = shift.apply(arguments); var firstArgUc = (typeof firstArg === "string") ? firstArg.charAt(0).toUpperCase() + firstArg.slice(1) : firstArg; } else { var firstArg = undefined; var firstArgUc = undefined; } var args = arguments; this.each(function() { // Grab ColorPicker object instance var instance = $(this).data('jQWCP.instance'); // Initialize if not yet created if(instance == undefined || instance == null) { // Get init options var options = {}; if(typeof firstArg === "object") { options = firstArg; } instance = new WCP.ColorPicker(this, options); $(this).data('jQWCP.instance', instance); } /// What to do? /// // No arguments provided, do nothing // wheelColorPicker() if(firstArg === undefined || typeof firstArg === "object") { } // Call a method // wheelColorPicker('show') else if(typeof instance[firstArg] === "function") { //console.log('method'); var ret = instance[firstArg].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Try option setter // wheelColorPicker('color', '#ff00aa') else if(typeof instance['set'+firstArgUc] === "function" && args.length > 0) { //console.log('setter'); var ret = instance['set'+firstArgUc].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Try option getter // wheelColorPicker('color') else if(typeof instance['get'+firstArgUc] === "function") { //console.log('getter'); var ret = instance['get'+firstArgUc].apply(instance, args); // If instance is not returned, no method chaining if(ret !== instance) { returnValue = ret; return false; } } // Set option value // wheelColorPicker('format', 'hex') else if(instance.options[firstArg] !== undefined && args.length > 0) { //console.log('set option'); instance.options[firstArg] = args[0]; } // Get option value // wheelColorPicker('format') else if(instance.options[firstArg] !== undefined) { //console.log('get option'); returnValue = instance.options[firstArg]; return false; } // Nothing matches, throw error else { $.error( 'Method/option named ' + firstArg + ' does not exist on jQuery.wheelColorPicker' ); } }); return returnValue; }; /******************************************************************/ ///////////////////////////////////////// // Shorthand for $.fn.wheelColorPicker // ///////////////////////////////////////// var WCP = $.fn.wheelColorPicker; ///////////////////////////////////////// /** * Object: defaults * * Contains default options for the wheelColorPicker plugin. * * Member properties: * * format - <string> Color naming style. See colorToRgb for all * available formats. * live - <boolean> Enable dynamic slider gradients. * preview - <boolean> Enable live color preview on input field * userinput - (Deprecated) <boolean> Enable picking color by typing directly * validate - <boolean> When userinput is enabled, force the value to be * a valid format. If user input an invalid color, the value * will be reverted to last valid color. * autoResize - <boolean> Automatically resize container width. * If set to false, you could manually adjust size with CSS. * autoFormat - <boolean> Automatically convert input value to * specified format. For example, if format is "rgb", * "#ff0000" will automatically converted into "rgb(255,0,0)". * color - <object|string> Initial value in any of supported color * value format or as an object. Setting this value will * override the current input value. * alpha - (Deprecated) <boolean> Force the color picker to use alpha value * despite its selected color format. This option is * deprecated. Use sliders = "a" instead. * inverseLabel - (deprecated) Boolean use inverse color for * input label instead of black/white color. * preserveWheel - Boolean preserve color wheel shade when slider * position changes. If set to true, changing * color wheel from black will reset selectedColor.val * (shade) to 1. * interactive - Boolean enable interactive sliders where slider bar * gradients change dynamically as user drag a slider * handle. Set to false if this affect performance. * See also 'quality' option if you wish to keep * interactive slider but with reduced quality. * cssClass - Object CSS Classes to be added to the color picker. * layout - String [block|popup] Layout mode. * animDuration - Number Duration for transitions such as fade-in * and fade-out. * quality - Rendering details quality. The normal quality is 1. * Setting less than 0.1 may make the sliders ugly, * while setting the value too high might affect performance. * sliders - String combination of sliders. If null then the color * picker will show default values, which is "wvp" for * normal color or "wvap" for color with alpha value. * Possible combinations are "whsvrgbap". * Order of letters will affect slider positions. * sliderLabel - Boolean Show labels for each slider. * sliderValue - Boolean Show numeric value of each slider. * hideKeyboard - Boolean Keep input blurred to avoid on screen keyboard appearing. * If this is set to true, avoid assigning handler to blur event. * rounding - Round the alpha value to N decimal digits. Default is 2. * Set -1 to disable rounding. * mobile - Display mobile-friendly layout when opened in mobile device. * mobileWidth - Max screen width to use mobile layout instead of default one. * mobileAutoScroll - Automatically scroll the page if focused input element * gets obstructed by color picker dialog. * htmlOptions - Load options from HTML attributes. * To set options using HTML attributes, * prefix these options with 'data-wcp-' as attribute names. * snap - Snap color wheel and slider on 0, 0.5, and 1.0 * snapTolerance - Snap if slider position falls within defined * tolerance value. */ WCP.defaults = { format: 'hex', /* 1.x */ preview: false, /* 1.x */ live: true, /* 2.0 */ userinput: true, /* DEPRECATED 1.x */ validate: true, /* 1.x */ autoResize: true, /* 3.0 */ autoFormat: true, /* 3.0 */ //color: null, /* DEPRECATED 1.x */ /* OBSOLETE 3.0 */ /* Init-time only */ //alpha: null, /* DEPRECATED 1.x */ /* OBSOLETE 3.0 */ /* See methods.alpha */ preserveWheel: null, /* DEPRECATED 1.x */ /* Use live */ cssClass: '', /* 2.0 */ layout: 'popup', /* 2.0 */ /* Init-time only */ animDuration: 200, /* 2.0 */ quality: 1, /* 2.0 */ sliders: null, /* 2.0 */ //sliderLabel: true, /* 2.0 */ /* NOT IMPLEMENTED */ //sliderValue: false, /* 2.0 */ /* NOT IMPLEMENTED */ rounding: 2, /* 2.3 */ mobile: true, /* 3.0 */ mobileWidth: 480, /* 3.0 */ hideKeyboard: false, /* 2.4 */ htmlOptions: true, /* 2.3 */ snap: false, /* 2.5 */ snapTolerance: 0.05 /* 2.5 */ }; /******************************************************************/ ////////////////////////////// // STATIC OBJECTS AND FLAGS // ////////////////////////////// /* * Note: To determine input position (top and left), use the following: * WCP.ORIGIN.top + this.input.getBoundingClientRect().top * instead of using $(this.input).offset().top because on mobile browsers * (chrome) jQuery's offset() function returns wrong value. */ /// Top left of the page is not on (0,0), making window.scrollX/Y and offset() useless /// See WCP.ORIGIN WCP.BUG_RELATIVE_PAGE_ORIGIN = false; /// Coordinate of the top left page (mobile chrome workaround) WCP.ORIGIN = { left: 0, top: 0 }; /******************************************************************/ ////////////////////// // HELPER FUNCTIONS // ////////////////////// /** * Function: colorToStr * * Since 2.0 * * Convert color object to string in specified format * * Available formats: * - hex * - hexa * - css * - cssa * - rgb * - rgb% * - rgba * - rgba% * - hsv * - hsv% * - hsva * - hsva% * - hsb * - hsb% * - hsba * - hsba% */ WCP.colorToStr = function( color, format ) { var result = ""; switch( format ) { case 'css': result = "#"; case 'hex': var r = Math.round(color.r * 255).toString(16); if( r.length == 1) { r = "0" + r; } var g = Math.round(color.g * 255).toString(16); if( g.length == 1) { g = "0" + g; } var b = Math.round(color.b * 255).toString(16); if( b.length == 1) { b = "0" + b; } result += r + g + b; break; case 'cssa': result = "#"; case 'hexa': var r = Math.round(color.r * 255).toString(16); if( r.length == 1) { r = "0" + r; } var g = Math.round(color.g * 255).toString(16); if( g.length == 1) { g = "0" + g; } var b = Math.round(color.b * 255).toString(16); if( b.length == 1) { b = "0" + b; } var a = Math.round(color.a * 255).toString(16); if( a.length == 1) { a = "0" + a; } result += r + g + b + a; break; case 'rgb': result = "rgb(" + Math.round(color.r * 255) + "," + Math.round(color.g * 255) + "," + Math.round(color.b * 255) + ")"; break; case 'rgb%': result = "rgb(" + (color.r * 100) + "%," + (color.g * 100) + "%," + (color.b * 100) + "%)"; break; case 'rgba': result = "rgba(" + Math.round(color.r * 255) + "," + Math.round(color.g * 255) + "," + Math.round(color.b * 255) + "," + color.a + ")"; break; case 'rgba%': result = "rgba(" + (color.r * 100) + "%," + (color.g * 100) + "%," + (color.b * 100) + "%," + (color.a * 100) + "%)"; break; case 'hsv': result = "hsv(" + (color.h * 360) + "," + color.s + "," + color.v + ")"; break; case 'hsv%': result = "hsv(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%)"; break; case 'hsva': result = "hsva(" + (color.h * 360) + "," + color.s + "," + color.v + "," + color.a + ")"; break; case 'hsva%': result = "hsva(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%," + (color.a * 100) + "%)"; break; case 'hsb': result = "hsb(" + color.h + "," + color.s + "," + color.v + ")"; break; case 'hsb%': result = "hsb(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%)"; break; case 'hsba': result = "hsba(" + color.h + "," + color.s + "," + color.v + "," + color.a + ")"; break; case 'hsba%': result = "hsba(" + (color.h * 100) + "%," + (color.s * 100) + "%," + (color.v * 100) + "%," + (color.a * 100) + "%)"; break; } return result; }; /** * Function: strToColor * * Since 2.0 * * Convert string to color object. * Please note that if RGB color is supplied, the returned value * will only contain RGB. * * If invalid string is supplied, FALSE will be returned. */ WCP.strToColor = function( val ) { var color = { a: 1 }; var tmp; var hasAlpha; // #fff // #ffff if( val.match(/^#[0-9a-f]{3}$/i) != null || val.match(/^#[0-9a-f]{4}$/i) ) { if( isNaN( color.r = parseInt(val.substr(1, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(2, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(3, 1), 16) * 17 / 255 ) ) { return false; } // Alpha if(val.length == 5) { if( isNaN( color.a = parseInt(val.substr(4, 1), 16) * 17 / 255 ) ) { return false; } } } // fff // ffff else if( val.match(/^[0-9a-f]{3}$/i) != null || val.match(/^[0-9a-f]{4}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(0, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(1, 1), 16) * 17 / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(2, 1), 16) * 17 / 255 ) ) { return false; } // Alpha if(val.length == 4) { if( isNaN( color.a = parseInt(val.substr(3, 1), 16) * 17 / 255 ) ) { return false; } } } // #ffffff // #ffffffff else if( val.match(/^#[0-9a-f]{6}$/i) != null || val.match(/^#[0-9a-f]{8}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(1, 2), 16) / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(3, 2), 16) / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(5, 2), 16) / 255 ) ) { return false; } // Alpha if(val.length == 9) { if( isNaN( color.a = parseInt(val.substr(7, 2), 16) / 255 ) ) { return false; } } } // ffffff // ffffffff else if( val.match(/^[0-9a-f]{6}$/i) != null || val.match(/^[0-9a-f]{8}$/i) != null ) { if( isNaN( color.r = parseInt(val.substr(0, 2), 16) / 255 ) ) { return false; } if( isNaN( color.g = parseInt(val.substr(2, 2), 16) / 255 ) ) { return false; } if( isNaN( color.b = parseInt(val.substr(4, 2), 16) / 255 ) ) { return false; } // Alpha if(val.length == 8) { if( isNaN( color.a = parseInt(val.substr(6, 2), 16) / 255 ) ) { return false; } } } // rgb(100%,100%,100%) // rgba(100%,100%,100%,100%) // rgba(255,255,255,1) // rgba(100%,1, 0.5,.2) else if( val.match(/^rgba\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^rgb\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null ) { if(val.match(/a/i) != null) { hasAlpha = true; } else { hasAlpha = false; } tmp = val.substring(val.indexOf('(')+1, val.indexOf(',')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.r = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.r = parseInt(tmp) / 255 ) ) { return false; } } tmp = val.substring(val.indexOf(',')+1, val.indexOf(',', val.indexOf(',')+1)); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.g = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.g = parseInt(tmp) / 255 ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.indexOf(',', val.indexOf(',')+1)+1, val.lastIndexOf(',')); } else { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); } if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.b = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.b = parseInt(tmp) / 255 ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.a = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.a = parseFloat(tmp) ) ) { return false; } } } } // hsv(100%,100%,100%) // hsva(100%,100%,100%,100%) // hsv(360,1,1,1) // hsva(360,1, 0.5,.2) // hsb(100%,100%,100%) // hsba(100%,100%,100%,100%) // hsb(360,1,1,1) // hsba(360,1, 0.5,.2) else if( val.match(/^hsva\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsv\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsba\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null || val.match(/^hsb\s*\(\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*,\s*([0-9\.]+%|[01]?\.?[0-9]*)\s*\)$/i) != null ) { if(val.match(/a/i) != null) { hasAlpha = true; } else { hasAlpha = false; } tmp = val.substring(val.indexOf('(')+1, val.indexOf(',')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.h = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.h = parseFloat(tmp) / 360 ) ) { return false; } } tmp = val.substring(val.indexOf(',')+1, val.indexOf(',', val.indexOf(',')+1)); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.s = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.s = parseFloat(tmp) ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.indexOf(',', val.indexOf(',')+1)+1, val.lastIndexOf(',')); } else { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); } if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.v = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.v = parseFloat(tmp) ) ) { return false; } } if(hasAlpha) { tmp = val.substring(val.lastIndexOf(',')+1, val.lastIndexOf(')')); if( tmp.charAt( tmp.length-1 ) == '%') { if( isNaN( color.a = parseFloat(tmp) / 100 ) ) { return false; } } else { if( isNaN( color.a = parseFloat(tmp) ) ) { return false; } } } } else { return false; } return color; }; /** * Function: hsvToRgb * * Since 2.0 * * Perform HSV to RGB conversion */ WCP.hsvToRgb = function( h, s, v ) { // Calculate RGB from hue (1st phase) var cr = (h <= (1/6) || h >= (5/6)) ? 1 : (h < (1/3) ? 1 - ((h - (1/6)) * 6) : (h > (4/6) ? (h - (4/6)) * 6 : 0)); var cg = (h >= (1/6) && h <= (3/6)) ? 1 : (h < (1/6) ? h * 6 : (h < (4/6) ? 1 - ((h - (3/6)) * 6) : 0)); var cb = (h >= (3/6) && h <= (5/6)) ? 1 : (h > (2/6) && h < (3/6) ? (h - (2/6)) * 6 : (h > (5/6) ? 1 - ((h - (5/6)) * 6) : 0)); // console.log(cr + ' ' + cg + ' ' + cb); // Calculate RGB with saturation & value applied var r = (cr + (1-cr)*(1-s)) * v; var g = (cg + (1-cg)*(1-s)) * v; var b = (cb + (1-cb)*(1-s)) * v; // console.log(r + ' ' + g + ' ' + b + ' ' + v); return { r: r, g: g, b: b }; }; /** * Function: rgbToHsv * * Since 2.0 * * Perform RGB to HSV conversion */ WCP.rgbToHsv = function( r, g, b ) { var h; var s; var v; var maxColor = Math.max(r, g, b); var minColor = Math.min(r, g, b); var delta = maxColor - minColor; // Calculate saturation if(maxColor != 0) { s = delta / maxColor; } else { s = 0; } // Calculate hue // To simplify the formula, we use 0-6 range. if(delta == 0) { h = 0; } else if(r == maxColor) { h = (6 + (g - b) / delta) % 6; } else if(g == maxColor) { h = 2 + (b - r) / delta; } else if(b == maxColor) { h = 4 + (r - g) / delta; } else { h = 0; } // Then adjust the range to be 0-1 h = h/6; // Calculate value v = maxColor; // console.log(h + ' ' + s + ' ' + v); return { h: h, s: s, v: v }; }; /******************************************************************/ //////////////////////// // COLOR PICKER CLASS // //////////////////////// /** * Class: ColorPicker * * Since 3.0 */ WCP.ColorPicker = function ( elm, options ) { // Assign reference to input DOM element this.input = elm; // Setup selected color in the following priority: // 1. options.color // 2. input.value // 3. default this.color = { h: 0, s: 0, v: 1, r: 1, g: 1, b: 1, a: 1 }; this.setValue(this.input.value); // Set options this.options = $.extend(true, {}, WCP.defaults); this.setOptions(options); // Check sliders option, if not defined, set default sliders if(this.options.sliders == null) this.options.sliders = 'wvp' + (this.options.format.indexOf('a') >= 0 ? 'a' : ''); this.init(); }; //////////////////// // Static members // //////////////////// /** * Static Property: ColorPicker.widget * * Reference to global color picker widget (popup) */ WCP.ColorPicker.widget = null; /** * Property: ColorPicker.overlay * * Reference to overlay DOM element (overlay for global popup) */ WCP.ColorPicker.overlay = null; /** * Function: init * * Since 3.0 * 2.0 was methods.staticInit * * Initialize wheel color picker globally. */ WCP.ColorPicker.init = function() { // Only perform initialization once if(WCP.ColorPicker.init.hasInit == true) return; WCP.ColorPicker.init.hasInit = true; // Insert overlay element to handle popup closing // when hideKeyboard is true, hence input is always blurred var $overlay = $('<div class="jQWCP-overlay" style="display: none;"></div>'); $overlay.on('click', WCP.Handler.overlay_click); WCP.ColorPicker.overlay = $overlay.get(0); $('body').append($overlay); // Insert CSS for color wheel var wheelImage = WCP.ColorPicker.getWheelDataUrl(200); $('head').append( '<style type="text/css">' + '.jQWCP-wWheel {' + 'background-image: url(' + wheelImage + ');' + '}' + '</style>' ); // Attach events $('html').on('mouseup.wheelColorPicker', WCP.Handler.html_mouseup); $('html').on('touchend.wheelColorPicker', WCP.Handler.html_mouseup); $('html').on('mousemove.wheelColorPicker', WCP.Handler.html_mousemove); $('html').on('touchmove.wheelColorPicker', WCP.Handler.html_mousemove); $(window).on('resize.wheelColorPicker', WCP.Handler.window_resize); }; /** * Function: createWidget * * Since 3.0 * 2.5 was private.initWidget * * Create color picker widget. */ WCP.ColorPicker.createWidget = function() { /// WIDGET /// // Notice: We won't use canvas to draw the color wheel since // it may takes time and cause performance issue. var $widget = $( "<div class='jQWCP-wWidget'>" + "<div class='jQWCP-wWheel'>" + "<div class='jQWCP-wWheelOverlay'></div>" + "<span class='jQWCP-wWheelCursor'></span>" + "</div>" + "<div class='jQWCP-wHue jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wHueSlider jQWCP-slider' width='1' height='50' title='Hue'></canvas>" + "<span class='jQWCP-wHueCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wSat jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wSatSlider jQWCP-slider' width='1' height='50' title='Saturation'></canvas>" + "<span class='jQWCP-wSatCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wVal jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wValSlider jQWCP-slider' width='1' height='50' title='Value'></canvas>" + "<span class='jQWCP-wValCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wRed jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wRedSlider jQWCP-slider' width='1' height='50' title='Red'></canvas>" + "<span class='jQWCP-wRedCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wGreen jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wGreenSlider jQWCP-slider' width='1' height='50' title='Green'></canvas>" + "<span class='jQWCP-wGreenCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wBlue jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wBlueSlider jQWCP-slider' width='1' height='50' title='Blue'></canvas>" + "<span class='jQWCP-wBlueCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wAlpha jQWCP-slider-wrapper'>" + "<canvas class='jQWCP-wAlphaSlider jQWCP-slider' width='1' height='50' title='Alpha'></canvas>" + "<span class='jQWCP-wAlphaCursor jQWCP-scursor'></span>" + "</div>" + "<div class='jQWCP-wPreview'>" + "<canvas class='jQWCP-wPreviewBox' width='1' height='1' title='Selected Color'></canvas>" + "</div>" + "</div>" ); // Small UI fix to disable highlighting the widget // Also UI fix to disable touch context menu $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-scursor, .jQWCP-slider') .attr('unselectable', 'on') .css('-moz-user-select', 'none') .css('-webkit-user-select', 'none') .css('user-select', 'none') .css('-webkit-touch-callout', 'none'); // Disable context menu on sliders // Workaround for touch browsers $widget.on('contextmenu.wheelColorPicker', function() { return false; }); // Bind widget events $widget.on('mousedown.wheelColorPicker', '.jQWCP-wWheel', WCP.Handler.wheel_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-wWheel', WCP.Handler.wheel_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-wWheelCursor', WCP.Handler.wheelCursor_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-wWheelCursor', WCP.Handler.wheelCursor_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-slider', WCP.Handler.slider_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-slider', WCP.Handler.slider_mousedown); $widget.on('mousedown.wheelColorPicker', '.jQWCP-scursor', WCP.Handler.sliderCursor_mousedown); $widget.on('touchstart.wheelColorPicker', '.jQWCP-scursor', WCP.Handler.sliderCursor_mousedown); return $widget.get(0); }; /** * Function: getWheelDataUrl * * Create color wheel image and return as base64 encoded data url. */ WCP.ColorPicker.getWheelDataUrl = function( size ) { var r = size / 2; // radius var center = r; var canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; var context = canvas.getContext('2d'); // Fill the wheel with colors for(var y = 0; y < size; y++) { for(var x = 0; x < size; x++) { // Get the offset from central position var offset = Math.sqrt(Math.pow(x - center, 2) + Math.pow(y - center, 2)); // Skip pixels outside picture area (plus 2 pixels) if(offset > r + 2) { continue; } // Get the position in degree (hue) var deg = ( (x - center == 0 ? (y < center ? 90 : 270) : (Math.atan((center - y) / (x - center)) / Math.PI * 180) ) + (x < center ? 180 : 0) + 360 ) % 360; // Relative offset (sat) var sat = offset / r; // Value is always 1 var val = 1; // Calculate color var cr = (Math.abs(deg + 360) + 60) % 360 < 120 ? 1 : (deg > 240 ? (120 - Math.abs(deg - 360)) / 60 : (deg < 120 ? (120 - deg) / 60 : 0)); var cg = Math.abs(deg - 120) < 60 ? 1 : (Math.abs(deg - 120) < 120 ? (120 - Math.abs(deg - 120)) / 60 : 0); var cb = Math.abs(deg - 240) < 60 ? 1 : (Math.abs(deg - 240) < 120 ? (120 - Math.abs(deg - 240)) / 60 : 0); var pr = Math.round((cr + (1 - cr) * (1 - sat)) * 255); var pg = Math.round((cg + (1 - cg) * (1 - sat)) * 255); var pb = Math.round((cb + (1 - cb) * (1 - sat)) * 255); context.fillStyle = 'rgb(' + pr + ',' + pg + ',' + pb + ')'; context.fillRect(x, y, 1, 1); } } return canvas.toDataURL(); }; ///////////// // Members // ///////////// /** * Property: ColorPicker.options * * Plugin options for the color picker instance, extended from WCP.defaults. */ WCP.ColorPicker.prototype.options = null; /** * Property: ColorPicker.input * * Reference to input DOM element */ WCP.ColorPicker.prototype.input = null; /** * Property: ColorPicker.widget * * Reference to widget DOM element (global popup or private inline widget) */ WCP.ColorPicker.prototype.widget = null; /** * Property: ColorPicker.color * * Selected color object. */ WCP.ColorPicker.prototype.color = null; /** * Property: ColorPicker.lastValue * * Store last input value */ WCP.ColorPicker.prototype.lastValue = null; /** * Function: ColorPicker.setOptions * * Since 3.0 * * Set options to the color picker. If htmlOptions is set to true, * options set via html attributes are also reloaded. If both html * attribute and argument exists, option set via options argument * gets priority. */ WCP.ColorPicker.prototype.setOptions = function( options ) { // options should be a separate object (passed by value) // Make a copy of options options = $.extend(true, {}, options); // Load options from HTML attributes if(this.options.htmlOptions) { for(var key in WCP.defaults) { // Only if option key is valid and not set via function argument if(this.input.hasAttribute('data-wcp-'+key) && options[key] === undefined) { options[key] = this.input.getAttribute('data-wcp-'+key); // Change true/false string to boolean if(options[key] == 'true') { options[key] = true; } else if(options[key] == 'false') { options[key] = false; } } } } // Set options for(var key in options) { // Skip undefined option key if(this.options[key] === undefined) continue; var keyUc = key.charAt(0).toUpperCase() + key.slice(1); // If setter is available, try setting it via setter if(typeof this['set'+keyUc] === "function") { this['set'+keyUc](options[key]); } // Otherwise directly update options else { this.options[key] = options[key]; } } return this; // Allow chaining }; /** * Function: ColorPicker.init * * Initialize wheel color picker widget */ WCP.ColorPicker.prototype.init = function() { WCP.ColorPicker.init(); // Initialization must only occur once if(this.hasInit == true) return; this.hasInit = true; var instance = this; var $input = $(this.input); var $widget = null; /// LAYOUT & BINDINGS /// // Setup block mode layout if( this.options.layout == 'block' ) { // Create widget this.widget = WCP.ColorPicker.createWidget(); $widget = $(this.widget); // Store object instance reference $widget.data('jQWCP.instance', this); // Wrap widget around the input elm and put the input // elm inside widget $widget.insertAfter(this.input); // Retain display CSS property if($input.css('display') == "inline") { $widget.css('display', "inline-block"); } else { $widget.css('display', $input.css('display')); } $widget.append(this.input); $input.hide(); // Add tabindex attribute to make the widget focusable if($input.attr('tabindex') != undefined) { $widget.attr('tabindex', $input.attr('tabindex')); } else { $widget.attr('tabindex', 0); } // Further widget adjustments based on options this.refreshWidget(); // Draw shading this.redrawSliders(true); this.updateSliders(); // Bind widget element events $widget.on('focus.wheelColorPicker', WCP.Handler.widget_focus_block); $widget.on('blur.wheelColorPicker', WCP.Handler.widget_blur_block); } // Setup popup mode layout else { // Only need to create one widget, used globally if(WCP.ColorPicker.widget == null) { WCP.ColorPicker.widget = WCP.ColorPicker.createWidget(); $widget = $(WCP.ColorPicker.widget); $widget.attr('id', 'jQWCP-popup'); // Assign widget to global $widget.hide(); $('body').append($widget); // Bind popup events $widget.on('mousedown.wheelColorPicker', WCP.Handler.widget_mousedown_popup); //$widget.on('mouseup.wheelColorPicker', WCP.Handler.widget_mouseup_popup); } this.widget = WCP.ColorPicker.widget; // Bind input element events $input.on('focus.wheelColorPicker', WCP.Handler.input_focus_popup); $input.on('blur.wheelColorPicker', WCP.Handler.input_blur_popup); } // Bind input events $input.on('keyup.wheelColorPicker', WCP.Handler.input_keyup); $input.on('change.wheelColorPicker', WCP.Handler.input_change); // Set color value // DEPRECATED by 3.0 if(typeof this.options.color == "object") { this.setColor(this.options.color); this.options.color = undefined; } else if(typeof this.options.color == "string") { this.setValue(this.options.color); this.options.color = undefined; } // Set readonly mode /* DEPRECATED */ if(this.options.userinput) { $input.removeAttr('readonly'); } else { $input.attr('readonly', true); } }; /** * Function: destroy * * Destroy the color picker and return it to normal element. */ WCP.ColorPicker.prototype.destroy = function() { var $widget = $(this.widget); var $input = $(this.input); // Reset layout // No need to delete global popup if(this.options.layout == 'block') { // Check if active control is the same widget as destroyed widget, remove the reference if it's true var $control = $( $('body').data('jQWCP.activeControl') ); // Refers to slider wrapper or wheel if ($control.length) { var controlWidget = $control.closest('.jQWCP-wWidget'); if ($widget.is(controlWidget)) { $('body').data('jQWCP.activeControl', null); } } $widget.before(this.input); $widget.remove(); $input.show(); } // Unbind events $input.off('focus.wheelColorPicker'); $input.off('blur.wheelColorPicker'); $input.off('keyup.wheelColorPicker'); $input.off('change.wheelColorPicker'); // Remove data $input.data('jQWCP.instance', null); // remove self delete this; }; /** * Function: refreshWidget * * Since 3.0 * 2.5 was private.adjustWidget * * Update widget to match current option values. */ WCP.ColorPicker.prototype.refreshWidget = function() { var $widget = $(this.widget); var options = this.options; var mobileLayout = false; // Set CSS classes $widget.attr('class', 'jQWCP-wWidget'); if(options.layout == 'block') { $widget.addClass('jQWCP-block'); } $widget.addClass(options.cssClass); //$widget.addClass(this.input.getAttribute('class')); // Check whether to use mobile layout if(window.innerWidth <= options.mobileWidth && options.layout != 'block' && options.mobile) { mobileLayout = true; $widget.addClass('jQWCP-mobile'); } // Rearrange sliders $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-wPreview') .hide() .addClass('hidden'); for(var i in options.sliders) { var $slider = null; switch(this.options.sliders[i]) { case 'w': $slider = $widget.find('.jQWCP-wWheel'); break; case 'h': $slider = $widget.find('.jQWCP-wHue'); break; case 's': $slider = $widget.find('.jQWCP-wSat'); break; case 'v': $slider = $widget.find('.jQWCP-wVal'); break; case 'r': $slider = $widget.find('.jQWCP-wRed'); break; case 'g': $slider = $widget.find('.jQWCP-wGreen'); break; case 'b': $slider = $widget.find('.jQWCP-wBlue'); break; case 'a': $slider = $widget.find('.jQWCP-wAlpha'); break; case 'p': $slider = $widget.find('.jQWCP-wPreview'); break; } if($slider != null) { $slider.appendTo(this.widget); $slider.show().removeClass('hidden'); } } // If widget is hidden, show it first so we can calculate dimensions correctly //var widgetIsHidden = false; //if($widget.is(':hidden')) { //widgetIsHidden = true; //$widget.css({ opacity: '0' }).show(); //} // Adjust sliders height based on quality var sliderHeight = options.quality * 50; $widget.find('.jQWCP-slider').attr('height', sliderHeight); var $visElms = $widget.find('.jQWCP-wWheel, .jQWCP-slider-wrapper, .jQWCP-wPreview').not('.hidden'); // Adjust container and sliders width // Only if not on mobile layout (force fixed on mobile) if(options.autoResize && !mobileLayout) { // Auto resize var width = 0 // Set slider size first, then adjust container $visElms.css({ width: '', height: '' }); $visElms.each(function(index, item) { var $item = $(item); width += parseFloat($item.css('margin-left').replace('px', '')) + parseFloat($item.css('margin-right').replace('px', '')) + $item.outerWidth(); }); $widget.css({ width: width + 'px' }); } else { // Fixed size // Set container size first, then adjust sliders $widget.css({ width: '' }); var $visWheel = $widget.find('.jQWCP-wWheel').not('.hidden'); var $visSliders = $widget.find('.jQWCP-slider-wrapper, .jQWCP-wPreview').not('.hidden'); $visWheel.css({ height: $widget.height() + 'px', width: $widget.height() }); if($visWheel.length > 0) { var horzSpace = $widget.width() - $visWheel.outerWidth() - parseFloat($visWheel.css('margin-left').replace('px', '')) - parseFloat($visWheel.css('margin-right').replace('px', '')); } else { var horzSpace = $widget.width(); } if($visSliders.length > 0) { var sliderMargins = parseFloat($visSliders.css('margin-left').replace('px', '')) + parseFloat($visSliders.css('margin-right').replace('px', '')); $visSliders.css({ height: $widget.height() + 'px', width: (horzSpace - ($visSliders.length - 1) * sliderMargins) / $visSliders.length + 'px' }); } } // Reset visibility //if(widgetIsHidden) { //$widget.css({ opacity: '' }).hide(); //} return this; // Allows method chaining }; /** * Function: redrawSliders * * Introduced in 2.0 * * Redraw slider gradients. Hidden sliders are not redrawn as to * improve performance. If options.live is FALSE, sliders are not redrawn. * * Parameter: * force - Boolean force redraw all sliders. */ WCP.ColorPicker.prototype.redrawSliders = function( force ) { // Skip if widget not yet initialized if(this.widget == null) return this; var $widget = $(this.widget); // DEPRECATED 3.0 // In 2.0, parameters are ( sliders, force ) if(typeof arguments[0] === "string") { force = arguments[1]; } // No need to redraw sliders on global popup widget if not // attached to the input elm in current iteration if(this != $widget.data('jQWCP.instance')) return this; var options = this.options; var color = this.color; var w = 1; var h = options.quality * 50; var A = 1; var R = 0; var G = 0; var B = 0; var H = 0; var S = 0; var V = 1; // Dynamic colors if(options.live) { A = color.a; R = Math.round(color.r * 255); G = Math.round(color.g * 255); B = Math.round(color.b * 255); H = color.h; S = color.s; V = color.v; } /// PREVIEW /// // Preview box must always be redrawn, if not hidden var $previewBox = $widget.find('.jQWCP-wPreviewBox'); if(!$previewBox.hasClass('hidden')) { var previewBoxCtx = $previewBox.get(0).getContext('2d'); previewBoxCtx.fillStyle = "rgba(" + R + "," + G + "," + B + "," + A + ")"; previewBoxCtx.clearRect(0, 0, 1, 1); previewBoxCtx.fillRect(0, 0, 1, 1); } /// SLIDERS /// if(!this.options.live && !force) return this; /// ALPHA /// // The top color is (R, G, B, 1) // The bottom color is (R, G, B, 0) var $alphaSlider = $widget.find('.jQWCP-wAlphaSlider'); if(!$alphaSlider.hasClass('hidden') || force) { var alphaSliderCtx = $alphaSlider.get(0).getContext('2d'); var alphaGradient = alphaSliderCtx.createLinearGradient(0, 0, 0, h); alphaGradient.addColorStop(0, "rgba("+R+","+G+","+B+",1)"); alphaGradient.addColorStop(1, "rgba("+R+","+G+","+B+",0)"); alphaSliderCtx.fillStyle = alphaGradient; alphaSliderCtx.clearRect(0, 0, w, h); alphaSliderCtx.fillRect(0, 0, w, h); } /// RED /// // The top color is (255, G, B) // The bottom color is (0, G, B) var $redSlider = $widget.find('.jQWCP-wRedSlider'); if(!$redSlider.hasClass('hidden') || force) { var redSliderCtx = $redSlider.get(0).getContext('2d'); var redGradient = redSliderCtx.createLinearGradient(0, 0, 0, h); redGradient.addColorStop(0, "rgb(255,"+G+","+B+")"); redGradient.addColorStop(1, "rgb(0,"+G+","+B+")"); redSliderCtx.fillStyle = redGradient; redSliderCtx.fillRect(0, 0, w, h); } /// GREEN /// // The top color is (R, 255, B) // The bottom color is (R, 0, B) var $greenSlider = $widget.find('.jQWCP-wGreenSlider'); if(!$greenSlider.hasClass('hidden') || force) { var greenSliderCtx = $greenSlider.get(0).getContext('2d'); var greenGradient = greenSliderCtx.createLinearGradient(0, 0, 0, h); greenGradient.addColorStop(0, "rgb("+R+",255,"+B+")"); greenGradient.addColorStop(1, "rgb("+R+",0,"+B+")"); greenSliderCtx.fillStyle = greenGradient; greenSliderCtx.fillRect(0, 0, w, h); } /// BLUE /// // The top color is (R, G, 255) // The bottom color is (R, G, 0) var $blueSlider = $widget.find('.jQWCP-wBlueSlider'); if(!$blueSlider.hasClass('hidden') || force) { var blueSliderCtx = $blueSlider.get(0).getContext('2d'); var blueGradient = blueSliderCtx.createLinearGradient(0, 0, 0, h); blueGradient.addColorStop(0, "rgb("+R+","+G+",255)"); blueGradient.addColorStop(1, "rgb("+R+","+G+",0)"); blueSliderCtx.fillStyle = blueGradient; blueSliderCtx.fillRect(0, 0, w, h); } /// HUE /// // The hue slider is static. var $hueSlider = $widget.find('.jQWCP-wHueSlider'); if(!$hueSlider.hasClass('hidden') || force) { var hueSliderCtx = $hueSlider.get(0).getContext('2d'); var hueGradient = hueSliderCtx.createLinearGradient(0, 0, 0, h); hueGradient.addColorStop(0, "#f00"); hueGradient.addColorStop(0.166666667, "#ff0"); hueGradient.addColorStop(0.333333333, "#0f0"); hueGradient.addColorStop(0.5, "#0ff"); hueGradient.addColorStop(0.666666667, "#00f"); hueGradient.addColorStop(0.833333333, "#f0f"); hueGradient.addColorStop(1, "#f00"); hueSliderCtx.fillStyle = hueGradient; hueSliderCtx.fillRect(0, 0, w, h); } /// SAT /// // The top color is hsv(h, 1, v) // The bottom color is hsv(0, 0, v) var $satSlider = $widget.find('.jQWCP-wSatSlider'); if(!$satSlider.hasClass('hidden') || force) { var satTopRgb = $.fn.wheelColorPicker.hsvToRgb(H, 1, V); satTopRgb.r = Math.round(satTopRgb.r * 255); satTopRgb.g = Math.round(satTopRgb.g * 255); satTopRgb.b = Math.round(satTopRgb.b * 255); var satSliderCtx = $satSlider.get(0).getContext('2d'); var satGradient = satSliderCtx.createLinearGradient(0, 0, 0, h); satGradient.addColorStop(0, "rgb("+satTopRgb.r+","+satTopRgb.g+","+satTopRgb.b+")"); satGradient.addColorStop(1, "rgb("+Math.round(V*255)+","+Math.round(V*255)+","+Math.round(V*255)+")"); satSliderCtx.fillStyle = satGradient; satSliderCtx.fillRect(0, 0, w, h); } /// VAL /// // The top color is hsv(h, s, 1) // The bottom color is always black. var $valSlider = $widget.find('.jQWCP-wValSlider'); if(!$valSlider.hasClass('hidden') || force) { var valTopRgb = $.fn.wheelColorPicker.hsvToRgb(H, S, 1); valTopRgb.r = Math.round(valTopRgb.r * 255); valTopRgb.g = Math.round(valTopRgb.g * 255); valTopRgb.b = Math.round(valTopRgb.b * 255); var valSliderCtx = $valSlider.get(0).getContext('2d'); var valGradient = valSliderCtx.createLinearGradient(0, 0, 0, h); valGradient.addColorStop(0, "rgb("+valTopRgb.r+","+valTopRgb.g+","+valTopRgb.b+")"); valGradient.addColorStop(1, "#000"); valSliderCtx.fillStyle = valGradient; valSliderCtx.fillRect(0, 0, w, h); } return this; // Allows method chaining }; /** * Function: updateSliders * * Introduced in 2.0 * * Update slider cursor positions based on this.color value. * Only displayed sliders are updated. This function shall be called when widget is displayed * so positions could be determined properly. */ WCP.ColorPicker.prototype.updateSliders = function() { // Skip if not yet initialized if(this.widget == null) return this; var $widget = $(this.widget); var color = this.color; // No need to redraw sliders on global popup widget if not // attached to the input elm in current iteration if(this != $widget.data('jQWCP.instance')) return this; // Wheel var $wheel = $widget.find('.jQWCP-wWheel'); if(!$wheel.hasClass('hidden')) { var $wheelCursor = $widget.find('.jQWCP-wWheelCursor'); var $wheelOverlay