diff --git a/.gitattributes b/.gitattributes index 545288622..ae2fa07cf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,7 @@ src/static/lightbox/* linguist-vendored src/static/typeahead/* linguist-vendored src/static/moment/* linguist-vendored src/static/datetimepicker/* linguist-vendored +src/static/colorpicker/* linguist-vendored src/static/charts/* linguist-vendored # Denote all files that are truly binary and should not be modified. diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 962693c08..808e4604c 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -602,7 +602,8 @@ class DisplaySettingsForm(SettingsForm): validators=[ RegexValidator(regex='^#[0-9a-fA-F]{6}$', message=_('Please enter the hexadecimal code of a color, e.g. #990000.')) - ] + ], + widget=forms.TextInput(attrs={'class': 'colorpickerfield'}) ) logo_image = ExtFileField( label=_('Logo image'), diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index d402d43ab..73dddb072 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -35,6 +35,7 @@ + {% endcompress %} {{ html_head|safe }} diff --git a/src/pretix/static/colorpicker/alpha-horizontal.png b/src/pretix/static/colorpicker/alpha-horizontal.png new file mode 100644 index 000000000..f83188951 Binary files /dev/null and b/src/pretix/static/colorpicker/alpha-horizontal.png differ diff --git a/src/pretix/static/colorpicker/alpha.png b/src/pretix/static/colorpicker/alpha.png new file mode 100644 index 000000000..2e53a30e7 Binary files /dev/null and b/src/pretix/static/colorpicker/alpha.png differ diff --git a/src/pretix/static/colorpicker/bootstrap-colorpicker.js b/src/pretix/static/colorpicker/bootstrap-colorpicker.js new file mode 100644 index 000000000..f923537e3 --- /dev/null +++ b/src/pretix/static/colorpicker/bootstrap-colorpicker.js @@ -0,0 +1,1319 @@ +/*! + * Bootstrap Colorpicker v2.5.1 + * https://itsjavi.com/bootstrap-colorpicker/ + * + * Originally written by (c) 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + * + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function(jq) { + return (factory(jq)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else if (jQuery && !jQuery.fn.colorpicker) { + factory(jQuery); + } +}(this, function($) { + 'use strict'; + /** + * Color manipulation helper class + * + * @param {Object|String} [val] + * @param {Object} [predefinedColors] + * @param {String|null} [fallbackColor] + * @param {String|null} [fallbackFormat] + * @param {Boolean} [hexNumberSignPrefix] + * @constructor + */ + var Color = function( + val, predefinedColors, fallbackColor, fallbackFormat, hexNumberSignPrefix) { + this.fallbackValue = fallbackColor ? + ( + fallbackColor && (typeof fallbackColor.h !== 'undefined') ? + fallbackColor : + this.value = { + h: 0, + s: 0, + b: 0, + a: 1 + } + ) : + null; + + this.fallbackFormat = fallbackFormat ? fallbackFormat : 'rgba'; + + this.hexNumberSignPrefix = hexNumberSignPrefix === true; + + this.value = this.fallbackValue; + + this.origFormat = null; // original string format + + this.predefinedColors = predefinedColors ? predefinedColors : {}; + + // We don't want to share aliases across instances so we extend new object + this.colors = $.extend({}, Color.webColors, this.predefinedColors); + + if (val) { + if (typeof val.h !== 'undefined') { + this.value = val; + } else { + this.setColor(String(val)); + } + } + + if (!this.value) { + // Initial value is always black if no arguments are passed or val is empty + this.value = { + h: 0, + s: 0, + b: 0, + a: 1 + }; + } + }; + + Color.webColors = { // 140 predefined colors from the HTML Colors spec + "aliceblue": "f0f8ff", + "antiquewhite": "faebd7", + "aqua": "00ffff", + "aquamarine": "7fffd4", + "azure": "f0ffff", + "beige": "f5f5dc", + "bisque": "ffe4c4", + "black": "000000", + "blanchedalmond": "ffebcd", + "blue": "0000ff", + "blueviolet": "8a2be2", + "brown": "a52a2a", + "burlywood": "deb887", + "cadetblue": "5f9ea0", + "chartreuse": "7fff00", + "chocolate": "d2691e", + "coral": "ff7f50", + "cornflowerblue": "6495ed", + "cornsilk": "fff8dc", + "crimson": "dc143c", + "cyan": "00ffff", + "darkblue": "00008b", + "darkcyan": "008b8b", + "darkgoldenrod": "b8860b", + "darkgray": "a9a9a9", + "darkgreen": "006400", + "darkkhaki": "bdb76b", + "darkmagenta": "8b008b", + "darkolivegreen": "556b2f", + "darkorange": "ff8c00", + "darkorchid": "9932cc", + "darkred": "8b0000", + "darksalmon": "e9967a", + "darkseagreen": "8fbc8f", + "darkslateblue": "483d8b", + "darkslategray": "2f4f4f", + "darkturquoise": "00ced1", + "darkviolet": "9400d3", + "deeppink": "ff1493", + "deepskyblue": "00bfff", + "dimgray": "696969", + "dodgerblue": "1e90ff", + "firebrick": "b22222", + "floralwhite": "fffaf0", + "forestgreen": "228b22", + "fuchsia": "ff00ff", + "gainsboro": "dcdcdc", + "ghostwhite": "f8f8ff", + "gold": "ffd700", + "goldenrod": "daa520", + "gray": "808080", + "green": "008000", + "greenyellow": "adff2f", + "honeydew": "f0fff0", + "hotpink": "ff69b4", + "indianred": "cd5c5c", + "indigo": "4b0082", + "ivory": "fffff0", + "khaki": "f0e68c", + "lavender": "e6e6fa", + "lavenderblush": "fff0f5", + "lawngreen": "7cfc00", + "lemonchiffon": "fffacd", + "lightblue": "add8e6", + "lightcoral": "f08080", + "lightcyan": "e0ffff", + "lightgoldenrodyellow": "fafad2", + "lightgrey": "d3d3d3", + "lightgreen": "90ee90", + "lightpink": "ffb6c1", + "lightsalmon": "ffa07a", + "lightseagreen": "20b2aa", + "lightskyblue": "87cefa", + "lightslategray": "778899", + "lightsteelblue": "b0c4de", + "lightyellow": "ffffe0", + "lime": "00ff00", + "limegreen": "32cd32", + "linen": "faf0e6", + "magenta": "ff00ff", + "maroon": "800000", + "mediumaquamarine": "66cdaa", + "mediumblue": "0000cd", + "mediumorchid": "ba55d3", + "mediumpurple": "9370d8", + "mediumseagreen": "3cb371", + "mediumslateblue": "7b68ee", + "mediumspringgreen": "00fa9a", + "mediumturquoise": "48d1cc", + "mediumvioletred": "c71585", + "midnightblue": "191970", + "mintcream": "f5fffa", + "mistyrose": "ffe4e1", + "moccasin": "ffe4b5", + "navajowhite": "ffdead", + "navy": "000080", + "oldlace": "fdf5e6", + "olive": "808000", + "olivedrab": "6b8e23", + "orange": "ffa500", + "orangered": "ff4500", + "orchid": "da70d6", + "palegoldenrod": "eee8aa", + "palegreen": "98fb98", + "paleturquoise": "afeeee", + "palevioletred": "d87093", + "papayawhip": "ffefd5", + "peachpuff": "ffdab9", + "peru": "cd853f", + "pink": "ffc0cb", + "plum": "dda0dd", + "powderblue": "b0e0e6", + "purple": "800080", + "red": "ff0000", + "rosybrown": "bc8f8f", + "royalblue": "4169e1", + "saddlebrown": "8b4513", + "salmon": "fa8072", + "sandybrown": "f4a460", + "seagreen": "2e8b57", + "seashell": "fff5ee", + "sienna": "a0522d", + "silver": "c0c0c0", + "skyblue": "87ceeb", + "slateblue": "6a5acd", + "slategray": "708090", + "snow": "fffafa", + "springgreen": "00ff7f", + "steelblue": "4682b4", + "tan": "d2b48c", + "teal": "008080", + "thistle": "d8bfd8", + "tomato": "ff6347", + "turquoise": "40e0d0", + "violet": "ee82ee", + "wheat": "f5deb3", + "white": "ffffff", + "whitesmoke": "f5f5f5", + "yellow": "ffff00", + "yellowgreen": "9acd32", + "transparent": "transparent" + }; + + Color.prototype = { + constructor: Color, + colors: {}, // merged web and predefined colors + predefinedColors: {}, + /** + * @return {Object} + */ + getValue: function() { + return this.value; + }, + /** + * @param {Object} val + */ + setValue: function(val) { + this.value = val; + }, + _sanitizeNumber: function(val) { + if (typeof val === 'number') { + return val; + } + if (isNaN(val) || (val === null) || (val === '') || (val === undefined)) { + return 1; + } + if (val === '') { + return 0; + } + if (typeof val.toLowerCase !== 'undefined') { + if (val.match(/^\./)) { + val = "0" + val; + } + return Math.ceil(parseFloat(val) * 100) / 100; + } + return 1; + }, + isTransparent: function(strVal) { + if (!strVal || !(typeof strVal === 'string' || strVal instanceof String)) { + return false; + } + strVal = strVal.toLowerCase().trim(); + return (strVal === 'transparent') || (strVal.match(/#?00000000/)) || (strVal.match(/(rgba|hsla)\(0,0,0,0?\.?0\)/)); + }, + rgbaIsTransparent: function(rgba) { + return ((rgba.r === 0) && (rgba.g === 0) && (rgba.b === 0) && (rgba.a === 0)); + }, + // parse a string to HSB + /** + * @protected + * @param {String} strVal + * @returns {boolean} Returns true if it could be parsed, false otherwise + */ + setColor: function(strVal) { + strVal = strVal.toLowerCase().trim(); + if (strVal) { + if (this.isTransparent(strVal)) { + this.value = { + h: 0, + s: 0, + b: 0, + a: 0 + }; + return true; + } else { + var parsedColor = this.parse(strVal); + if (parsedColor) { + this.value = this.value = { + h: parsedColor.h, + s: parsedColor.s, + b: parsedColor.b, + a: parsedColor.a + }; + if (!this.origFormat) { + this.origFormat = parsedColor.format; + } + } else if (this.fallbackValue) { + // if parser fails, defaults to fallbackValue if defined, otherwise the value won't be changed + this.value = this.fallbackValue; + } + } + } + return false; + }, + setHue: function(h) { + this.value.h = 1 - h; + }, + setSaturation: function(s) { + this.value.s = s; + }, + setBrightness: function(b) { + this.value.b = 1 - b; + }, + setAlpha: function(a) { + this.value.a = Math.round((parseInt((1 - a) * 100, 10) / 100) * 100) / 100; + }, + toRGB: function(h, s, b, a) { + if (arguments.length === 0) { + h = this.value.h; + s = this.value.s; + b = this.value.b; + a = this.value.a; + } + + 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 + }; + }, + toHex: function(h, s, b, a) { + if (arguments.length === 0) { + h = this.value.h; + s = this.value.s; + b = this.value.b; + a = this.value.a; + } + + var rgb = this.toRGB(h, s, b, a); + + if (this.rgbaIsTransparent(rgb)) { + return 'transparent'; + } + + var hexStr = (this.hexNumberSignPrefix ? '#' : '') + ( + (1 << 24) + + (parseInt(rgb.r) << 16) + + (parseInt(rgb.g) << 8) + + parseInt(rgb.b)) + .toString(16) + .slice(1); + + return hexStr; + }, + toHSL: function(h, s, b, a) { + if (arguments.length === 0) { + h = this.value.h; + s = this.value.s; + b = this.value.b; + a = this.value.a; + } + + var H = h, + L = (2 - s) * b, + S = s * b; + if (L > 0 && L <= 1) { + S /= L; + } else { + S /= 2 - L; + } + L /= 2; + if (S > 1) { + S = 1; + } + return { + h: isNaN(H) ? 0 : H, + s: isNaN(S) ? 0 : S, + l: isNaN(L) ? 0 : L, + a: isNaN(a) ? 0 : a + }; + }, + toAlias: function(r, g, b, a) { + var c, rgb = (arguments.length === 0) ? this.toHex() : this.toHex(r, g, b, a); + + // support predef. colors in non-hex format too, as defined in the alias itself + var original = this.origFormat === 'alias' ? rgb : this.toString(this.origFormat, false); + + for (var alias in this.colors) { + c = this.colors[alias].toLowerCase().trim(); + if ((c === rgb) || (c === original)) { + return alias; + } + } + return false; + }, + 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: this._sanitizeNumber(H), + s: S, + b: V, + a: this._sanitizeNumber(a) + }; + }, + HueToRGB: function(p, q, h) { + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + if ((h * 6) < 1) { + return p + (q - p) * h * 6; + } else if ((h * 2) < 1) { + return q; + } else if ((h * 3) < 2) { + return p + (q - p) * ((2 / 3) - h) * 6; + } else { + return p; + } + }, + HSLtoRGB: function(h, s, l, a) { + if (s < 0) { + s = 0; + } + var q; + if (l <= 0.5) { + q = l * (1 + s); + } else { + q = l + s - (l * s); + } + + var p = 2 * l - q; + + var tr = h + (1 / 3); + var tg = h; + var tb = h - (1 / 3); + + var r = Math.round(this.HueToRGB(p, q, tr) * 255); + var g = Math.round(this.HueToRGB(p, q, tg) * 255); + var b = Math.round(this.HueToRGB(p, q, tb) * 255); + return [r, g, b, this._sanitizeNumber(a)]; + }, + /** + * @param {String} strVal + * @returns {Object} Object containing h,s,b,a,format properties or FALSE if failed to parse + */ + parse: function(strVal) { + if (arguments.length === 0) { + return false; + } + + var that = this, + result = false, + isAlias = (typeof this.colors[strVal] !== 'undefined'), + values, format; + + if (isAlias) { + strVal = this.colors[strVal].toLowerCase().trim(); + } + + $.each(this.stringParsers, function(i, parser) { + var match = parser.re.exec(strVal); + values = match && parser.parse.apply(that, [match]); + if (values) { + result = {}; + format = (isAlias ? 'alias' : (parser.format ? parser.format : that.getValidFallbackFormat())); + if (format.match(/hsla?/)) { + result = that.RGBtoHSB.apply(that, that.HSLtoRGB.apply(that, values)); + } else { + result = that.RGBtoHSB.apply(that, values); + } + if (result instanceof Object) { + result.format = format; + } + return false; // stop iterating + } + return true; + }); + return result; + }, + getValidFallbackFormat: function() { + var formats = [ + 'rgba', 'rgb', 'hex', 'hsla', 'hsl' + ]; + if (this.origFormat && (formats.indexOf(this.origFormat) !== -1)) { + return this.origFormat; + } + if (this.fallbackFormat && (formats.indexOf(this.fallbackFormat) !== -1)) { + return this.fallbackFormat; + } + + return 'rgba'; // By default, return a format that will not lose the alpha info + }, + /** + * + * @param {string} [format] (default: rgba) + * @param {boolean} [translateAlias] Return real color for pre-defined (non-standard) aliases (default: false) + * @returns {String} + */ + toString: function(format, translateAlias) { + format = format || this.origFormat || this.fallbackFormat; + translateAlias = translateAlias || false; + + var c = false; + + switch (format) { + case 'rgb': + { + c = this.toRGB(); + if (this.rgbaIsTransparent(c)) { + return 'transparent'; + } + return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; + } + break; + case 'rgba': + { + c = this.toRGB(); + return 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + c.a + ')'; + } + break; + case 'hsl': + { + c = this.toHSL(); + return 'hsl(' + Math.round(c.h * 360) + ',' + Math.round(c.s * 100) + '%,' + Math.round(c.l * 100) + '%)'; + } + break; + case 'hsla': + { + c = this.toHSL(); + return 'hsla(' + Math.round(c.h * 360) + ',' + Math.round(c.s * 100) + '%,' + Math.round(c.l * 100) + '%,' + c.a + ')'; + } + break; + case 'hex': + { + return this.toHex(); + } + break; + case 'alias': + { + c = this.toAlias(); + + if (c === false) { + return this.toString(this.getValidFallbackFormat()); + } + + if (translateAlias && !(c in Color.webColors) && (c in this.predefinedColors)) { + return this.predefinedColors[c]; + } + + return c; + } + default: + { + return c; + } + break; + } + }, + // a set of RE's that can match strings and generate color tuples. + // from John Resig color plugin + // https://github.com/jquery/jquery-color/ + stringParsers: [{ + re: /rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*?\)/, + format: 'rgb', + parse: function(execResult) { + return [ + execResult[1], + execResult[2], + execResult[3], + 1 + ]; + } + }, { + re: /rgb\(\s*(\d*(?:\.\d+)?)\%\s*,\s*(\d*(?:\.\d+)?)\%\s*,\s*(\d*(?:\.\d+)?)\%\s*?\)/, + format: 'rgb', + parse: function(execResult) { + return [ + 2.55 * execResult[1], + 2.55 * execResult[2], + 2.55 * execResult[3], + 1 + ]; + } + }, { + re: /rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d*(?:\.\d+)?)\s*)?\)/, + format: 'rgba', + 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*)?\)/, + format: 'rgba', + parse: function(execResult) { + return [ + 2.55 * execResult[1], + 2.55 * execResult[2], + 2.55 * execResult[3], + execResult[4] + ]; + } + }, { + re: /hsl\(\s*(\d*(?:\.\d+)?)\s*,\s*(\d*(?:\.\d+)?)\%\s*,\s*(\d*(?:\.\d+)?)\%\s*?\)/, + format: 'hsl', + parse: function(execResult) { + return [ + execResult[1] / 360, + execResult[2] / 100, + execResult[3] / 100, + execResult[4] + ]; + } + }, { + re: /hsla\(\s*(\d*(?:\.\d+)?)\s*,\s*(\d*(?:\.\d+)?)\%\s*,\s*(\d*(?:\.\d+)?)\%\s*(?:,\s*(\d*(?:\.\d+)?)\s*)?\)/, + format: 'hsla', + parse: function(execResult) { + return [ + execResult[1] / 360, + execResult[2] / 100, + execResult[3] / 100, + execResult[4] + ]; + } + }, { + re: /#?([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, + format: 'hex', + parse: function(execResult) { + return [ + parseInt(execResult[1], 16), + parseInt(execResult[2], 16), + parseInt(execResult[3], 16), + 1 + ]; + } + }, { + re: /#?([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/, + format: 'hex', + parse: function(execResult) { + return [ + parseInt(execResult[1] + execResult[1], 16), + parseInt(execResult[2] + execResult[2], 16), + parseInt(execResult[3] + execResult[3], 16), + 1 + ]; + } + }], + colorNameToHex: function(name) { + if (typeof this.colors[name.toLowerCase()] !== 'undefined') { + return this.colors[name.toLowerCase()]; + } + return false; + } + }; + + /* + * Default plugin options + */ + var defaults = { + horizontal: false, // horizontal mode layout ? + inline: false, //forces to show the colorpicker as an inline element + color: false, //forces a color + format: false, //forces a format + input: 'input', // children input selector + container: false, // container selector + component: '.add-on, .input-group-addon', // children component selector + fallbackColor: false, // fallback color value. null = keeps current color. + fallbackFormat: 'hex', // fallback color format + hexNumberSignPrefix: true, // put a '#' (number sign) before hex strings + sliders: { + saturation: { + maxLeft: 100, + maxTop: 100, + callLeft: 'setSaturation', + callTop: 'setBrightness' + }, + hue: { + maxLeft: 0, + maxTop: 100, + callLeft: false, + callTop: 'setHue' + }, + alpha: { + maxLeft: 0, + maxTop: 100, + callLeft: false, + callTop: 'setAlpha' + } + }, + slidersHorz: { + saturation: { + maxLeft: 100, + maxTop: 100, + callLeft: 'setSaturation', + callTop: 'setBrightness' + }, + hue: { + maxLeft: 100, + maxTop: 0, + callLeft: 'setHue', + callTop: false + }, + alpha: { + maxLeft: 100, + maxTop: 0, + callLeft: 'setAlpha', + callTop: false + } + }, + template: '
', + align: 'right', + customClass: null, // custom class added to the colorpicker element + colorSelectors: null // custom color aliases + }; + + /** + * Colorpicker component class + * + * @param {Object|String} element + * @param {Object} options + * @constructor + */ + var Colorpicker = function(element, options) { + this.element = $(element).addClass('colorpicker-element'); + this.options = $.extend(true, {}, defaults, this.element.data(), options); + this.component = this.options.component; + this.component = (this.component !== false) ? this.element.find(this.component) : false; + if (this.component && (this.component.length === 0)) { + this.component = false; + } + this.container = (this.options.container === true) ? this.element : this.options.container; + this.container = (this.container !== false) ? $(this.container) : false; + + // Is the element an input? Should we search inside for any input? + this.input = this.element.is('input') ? this.element : (this.options.input ? + this.element.find(this.options.input) : false); + if (this.input && (this.input.length === 0)) { + this.input = false; + } + // Set HSB color + this.color = this.createColor(this.options.color !== false ? this.options.color : this.getValue()); + + this.format = this.options.format !== false ? this.options.format : this.color.origFormat; + + if (this.options.color !== false) { + this.updateInput(this.color); + this.updateData(this.color); + } + + // Setup picker + var $picker = this.picker = $(this.options.template); + if (this.options.customClass) { + $picker.addClass(this.options.customClass); + } + if (this.options.inline) { + $picker.addClass('colorpicker-inline colorpicker-visible'); + } else { + $picker.addClass('colorpicker-hidden'); + } + if (this.options.horizontal) { + $picker.addClass('colorpicker-horizontal'); + } + if ( + (['rgba', 'hsla', 'alias'].indexOf(this.format) !== -1) || + this.options.format === false || + this.getValue() === 'transparent' + ) { + $picker.addClass('colorpicker-with-alpha'); + } + if (this.options.align === 'right') { + $picker.addClass('colorpicker-right'); + } + if (this.options.inline === true) { + $picker.addClass('colorpicker-no-arrow'); + } + if (this.options.colorSelectors) { + var colorpicker = this, + selectorsContainer = colorpicker.picker.find('.colorpicker-selectors'); + + if (selectorsContainer.length > 0) { + $.each(this.options.colorSelectors, function(name, color) { + var $btn = $('') + .addClass('colorpicker-selectors-color') + .css('background-color', color) + .data('class', name).data('alias', name); + + $btn.on('mousedown.colorpicker touchstart.colorpicker', function(event) { + event.preventDefault(); + colorpicker.setValue( + colorpicker.format === 'alias' ? $(this).data('alias') : $(this).css('background-color') + ); + }); + selectorsContainer.append($btn); + }); + selectorsContainer.show().addClass('colorpicker-visible'); + } + } + + // Prevent closing the colorpicker when clicking on itself + $picker.on('mousedown.colorpicker touchstart.colorpicker', $.proxy(function(e) { + if (e.target === e.currentTarget) { + e.preventDefault(); + } + }, this)); + + // Bind click/tap events on the sliders + $picker.find('.colorpicker-saturation, .colorpicker-hue, .colorpicker-alpha') + .on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.mousedown, this)); + + $picker.appendTo(this.container ? this.container : $('body')); + + // Bind other events + if (this.input !== false) { + this.input.on({ + 'keyup.colorpicker': $.proxy(this.keyup, this) + }); + this.input.on({ + 'change.colorpicker': $.proxy(this.change, this) + }); + if (this.component === false) { + this.element.on({ + 'focus.colorpicker': $.proxy(this.show, this) + }); + } + if (this.options.inline === false) { + this.element.on({ + 'focusout.colorpicker': $.proxy(this.hide, this) + }); + } + } + + if (this.component !== false) { + this.component.on({ + 'click.colorpicker': $.proxy(this.show, this) + }); + } + + if ((this.input === false) && (this.component === false)) { + this.element.on({ + 'click.colorpicker': $.proxy(this.show, this) + }); + } + + // for HTML5 input[type='color'] + if ((this.input !== false) && (this.component !== false) && (this.input.attr('type') === 'color')) { + + this.input.on({ + 'click.colorpicker': $.proxy(this.show, this), + 'focus.colorpicker': $.proxy(this.show, this) + }); + } + this.update(); + + $($.proxy(function() { + this.element.trigger('create'); + }, this)); + }; + + Colorpicker.Color = Color; + + Colorpicker.prototype = { + constructor: Colorpicker, + destroy: function() { + this.picker.remove(); + this.element.removeData('colorpicker', 'color').off('.colorpicker'); + if (this.input !== false) { + this.input.off('.colorpicker'); + } + if (this.component !== false) { + this.component.off('.colorpicker'); + } + this.element.removeClass('colorpicker-element'); + this.element.trigger({ + type: 'destroy' + }); + }, + reposition: function() { + if (this.options.inline !== false || this.options.container) { + return false; + } + var type = this.container && this.container[0] !== window.document.body ? 'position' : 'offset'; + var element = this.component || this.element; + var offset = element[type](); + if (this.options.align === 'right') { + offset.left -= this.picker.outerWidth() - element.outerWidth(); + } + this.picker.css({ + top: offset.top + element.outerHeight(), + left: offset.left + }); + }, + show: function(e) { + if (this.isDisabled()) { + // Don't show the widget if it's disabled (the input) + return; + } + this.picker.addClass('colorpicker-visible').removeClass('colorpicker-hidden'); + this.reposition(); + $(window).on('resize.colorpicker', $.proxy(this.reposition, this)); + if (e && (!this.hasInput() || this.input.attr('type') === 'color')) { + if (e.stopPropagation && e.preventDefault) { + e.stopPropagation(); + e.preventDefault(); + } + } + if ((this.component || !this.input) && (this.options.inline === false)) { + $(window.document).on({ + 'mousedown.colorpicker': $.proxy(this.hide, this) + }); + } + this.element.trigger({ + type: 'showPicker', + color: this.color + }); + }, + hide: function(e) { + if ((typeof e !== 'undefined') && e.target) { + // Prevent hide if triggered by an event and an element inside the colorpicker has been clicked/touched + if ( + $(e.currentTarget).parents('.colorpicker').length > 0 || + $(e.target).parents('.colorpicker').length > 0 + ) { + return false; + } + } + this.picker.addClass('colorpicker-hidden').removeClass('colorpicker-visible'); + $(window).off('resize.colorpicker', this.reposition); + $(window.document).off({ + 'mousedown.colorpicker': this.hide + }); + this.update(); + this.element.trigger({ + type: 'hidePicker', + color: this.color + }); + }, + updateData: function(val) { + val = val || this.color.toString(this.format, false); + this.element.data('color', val); + return val; + }, + updateInput: function(val) { + val = val || this.color.toString(this.format, false); + if (this.input !== false) { + this.input.prop('value', val); + this.input.trigger('change'); + } + return val; + }, + updatePicker: function(val) { + if (typeof val !== 'undefined') { + this.color = this.createColor(val); + } + var sl = (this.options.horizontal === false) ? this.options.sliders : this.options.slidersHorz; + var icns = this.picker.find('i'); + if (icns.length === 0) { + return; + } + if (this.options.horizontal === false) { + sl = this.options.sliders; + icns.eq(1).css('top', sl.hue.maxTop * (1 - this.color.value.h)).end() + .eq(2).css('top', sl.alpha.maxTop * (1 - this.color.value.a)); + } else { + sl = this.options.slidersHorz; + icns.eq(1).css('left', sl.hue.maxLeft * (1 - this.color.value.h)).end() + .eq(2).css('left', sl.alpha.maxLeft * (1 - this.color.value.a)); + } + icns.eq(0).css({ + 'top': sl.saturation.maxTop - this.color.value.b * sl.saturation.maxTop, + 'left': this.color.value.s * sl.saturation.maxLeft + }); + + this.picker.find('.colorpicker-saturation') + .css('backgroundColor', (this.options.hexNumberSignPrefix ? '' : '#') + this.color.toHex(this.color.value.h, 1, 1, 1)); + + this.picker.find('.colorpicker-alpha') + .css('backgroundColor', (this.options.hexNumberSignPrefix ? '' : '#') + this.color.toHex()); + + this.picker.find('.colorpicker-color, .colorpicker-color div') + .css('backgroundColor', this.color.toString(this.format, true)); + + return val; + }, + updateComponent: function(val) { + var color; + + if (typeof val !== 'undefined') { + color = this.createColor(val); + } else { + color = this.color; + } + + if (this.component !== false) { + var icn = this.component.find('i').eq(0); + if (icn.length > 0) { + icn.css({ + 'backgroundColor': color.toString(this.format, true) + }); + } else { + this.component.css({ + 'backgroundColor': color.toString(this.format, true) + }); + } + } + + return color.toString(this.format, false); + }, + update: function(force) { + var val; + if ((this.getValue(false) !== false) || (force === true)) { + // Update input/data only if the current value is not empty + val = this.updateComponent(); + this.updateInput(val); + this.updateData(val); + this.updatePicker(); // only update picker if value is not empty + } + return val; + + }, + setValue: function(val) { // set color manually + this.color = this.createColor(val); + this.update(true); + this.element.trigger({ + type: 'changeColor', + color: this.color, + value: val + }); + }, + /** + * Creates a new color using the instance options + * @protected + * @param {String} val + * @returns {Color} + */ + createColor: function(val) { + return new Color( + val ? val : null, + this.options.colorSelectors, + this.options.fallbackColor ? this.options.fallbackColor : this.color, + this.options.fallbackFormat, + this.options.hexNumberSignPrefix + ); + }, + getValue: function(defaultValue) { + defaultValue = (typeof defaultValue === 'undefined') ? this.options.fallbackColor : defaultValue; + var val; + if (this.hasInput()) { + val = this.input.val(); + } else { + val = this.element.data('color'); + } + if ((val === undefined) || (val === '') || (val === null)) { + // if not defined or empty, return default + val = defaultValue; + } + return val; + }, + hasInput: function() { + return (this.input !== false); + }, + isDisabled: function() { + if (this.hasInput()) { + return (this.input.prop('disabled') === true); + } + return false; + }, + disable: function() { + if (this.hasInput()) { + this.input.prop('disabled', true); + this.element.trigger({ + type: 'disable', + color: this.color, + value: this.getValue() + }); + return true; + } + return false; + }, + enable: function() { + if (this.hasInput()) { + this.input.prop('disabled', false); + this.element.trigger({ + type: 'enable', + color: this.color, + value: this.getValue() + }); + return true; + } + return false; + }, + currentSlider: null, + mousePointer: { + left: 0, + top: 0 + }, + mousedown: function(e) { + if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) { + e.pageX = e.originalEvent.touches[0].pageX; + e.pageY = e.originalEvent.touches[0].pageY; + } + e.stopPropagation(); + e.preventDefault(); + + var target = $(e.target); + + //detect the slider and set the limits and callbacks + var zone = target.closest('div'); + var sl = this.options.horizontal ? this.options.slidersHorz : this.options.sliders; + if (!zone.is('.colorpicker')) { + if (zone.is('.colorpicker-saturation')) { + this.currentSlider = $.extend({}, sl.saturation); + } else if (zone.is('.colorpicker-hue')) { + this.currentSlider = $.extend({}, sl.hue); + } else if (zone.is('.colorpicker-alpha')) { + this.currentSlider = $.extend({}, sl.alpha); + } else { + return false; + } + var offset = zone.offset(); + //reference to guide's style + this.currentSlider.guide = zone.find('i')[0].style; + this.currentSlider.left = e.pageX - offset.left; + this.currentSlider.top = e.pageY - offset.top; + this.mousePointer = { + left: e.pageX, + top: e.pageY + }; + //trigger mousemove to move the guide to the current position + $(window.document).on({ + 'mousemove.colorpicker': $.proxy(this.mousemove, this), + 'touchmove.colorpicker': $.proxy(this.mousemove, this), + 'mouseup.colorpicker': $.proxy(this.mouseup, this), + 'touchend.colorpicker': $.proxy(this.mouseup, this) + }).trigger('mousemove'); + } + return false; + }, + mousemove: function(e) { + if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) { + e.pageX = e.originalEvent.touches[0].pageX; + e.pageY = e.originalEvent.touches[0].pageY; + } + e.stopPropagation(); + e.preventDefault(); + var left = Math.max( + 0, + Math.min( + this.currentSlider.maxLeft, + this.currentSlider.left + ((e.pageX || this.mousePointer.left) - this.mousePointer.left) + ) + ); + var top = Math.max( + 0, + Math.min( + this.currentSlider.maxTop, + this.currentSlider.top + ((e.pageY || this.mousePointer.top) - this.mousePointer.top) + ) + ); + this.currentSlider.guide.left = left + 'px'; + this.currentSlider.guide.top = top + 'px'; + if (this.currentSlider.callLeft) { + this.color[this.currentSlider.callLeft].call(this.color, left / this.currentSlider.maxLeft); + } + if (this.currentSlider.callTop) { + this.color[this.currentSlider.callTop].call(this.color, top / this.currentSlider.maxTop); + } + // Change format dynamically + // Only occurs if user choose the dynamic format by + // setting option format to false + if ( + this.options.format === false && + (this.currentSlider.callTop === 'setAlpha' || + this.currentSlider.callLeft === 'setAlpha') + ) { + + // Converting from hex / rgb to rgba + if (this.color.value.a !== 1) { + this.format = 'rgba'; + this.color.origFormat = 'rgba'; + } + + // Converting from rgba to hex + else { + this.format = 'hex'; + this.color.origFormat = 'hex'; + } + } + this.update(true); + + this.element.trigger({ + type: 'changeColor', + color: this.color + }); + return false; + }, + mouseup: function(e) { + e.stopPropagation(); + e.preventDefault(); + $(window.document).off({ + 'mousemove.colorpicker': this.mousemove, + 'touchmove.colorpicker': this.mousemove, + 'mouseup.colorpicker': this.mouseup, + 'touchend.colorpicker': this.mouseup + }); + return false; + }, + change: function(e) { + this.keyup(e); + }, + keyup: function(e) { + if ((e.keyCode === 38)) { + if (this.color.value.a < 1) { + this.color.value.a = Math.round((this.color.value.a + 0.01) * 100) / 100; + } + this.update(true); + } else if ((e.keyCode === 40)) { + if (this.color.value.a > 0) { + this.color.value.a = Math.round((this.color.value.a - 0.01) * 100) / 100; + } + this.update(true); + } else { + this.color = this.createColor(this.input.val()); + // Change format dynamically + // Only occurs if user choose the dynamic format by + // setting option format to false + if (this.color.origFormat && this.options.format === false) { + this.format = this.color.origFormat; + } + if (this.getValue(false) !== false) { + this.updateData(); + this.updateComponent(); + this.updatePicker(); + } + } + this.element.trigger({ + type: 'changeColor', + color: this.color, + value: this.input.val() + }); + } + }; + + $.colorpicker = Colorpicker; + + $.fn.colorpicker = function(option) { + var apiArgs = Array.prototype.slice.call(arguments, 1), + isSingleElement = (this.length === 1), + returnValue = null; + + var $jq = this.each(function() { + var $this = $(this), + inst = $this.data('colorpicker'), + options = ((typeof option === 'object') ? option : {}); + + if (!inst) { + inst = new Colorpicker(this, options); + $this.data('colorpicker', inst); + } + + if (typeof option === 'string') { + if ($.isFunction(inst[option])) { + returnValue = inst[option].apply(inst, apiArgs); + } else { // its a property ? + if (apiArgs.length) { + // set property + inst[option] = apiArgs[0]; + } + returnValue = inst[option]; + } + } else { + returnValue = $this; + } + }); + return isSingleElement ? returnValue : $jq; + }; + + $.fn.colorpicker.constructor = Colorpicker; + +})); diff --git a/src/pretix/static/colorpicker/bootstrap-colorpicker.scss b/src/pretix/static/colorpicker/bootstrap-colorpicker.scss new file mode 100644 index 000000000..616630a55 --- /dev/null +++ b/src/pretix/static/colorpicker/bootstrap-colorpicker.scss @@ -0,0 +1,230 @@ +/*! + * Bootstrap Colorpicker v2.5.1 + * https://itsjavi.com/bootstrap-colorpicker/ + * + * Originally written by (c) 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + * + */ +.colorpicker-saturation { + width: 100px; + height: 100px; + background-image: url(static('colorpicker/saturation.png')); + cursor: crosshair; + float: left; +} +.colorpicker-saturation i { + display: block; + height: 5px; + width: 5px; + border: 1px solid #000; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + position: absolute; + top: 0; + left: 0; + margin: -4px 0 0 -4px; +} +.colorpicker-saturation i b { + display: block; + height: 5px; + width: 5px; + border: 1px solid #fff; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.colorpicker-hue, +.colorpicker-alpha { + width: 15px; + height: 100px; + float: left; + cursor: row-resize; + margin-left: 4px; + margin-bottom: 4px; +} +.colorpicker-hue i, +.colorpicker-alpha i { + display: block; + height: 1px; + background: #000; + border-top: 1px solid #fff; + position: absolute; + top: 0; + left: 0; + width: 100%; + margin-top: -1px; +} +.colorpicker-hue { + background-image: url(static('colorpicker/hue.png')); +} +.colorpicker-alpha { + background-image: url(static('colorpicker/alpha.png')); + display: none; +} +.colorpicker-saturation, +.colorpicker-hue, +.colorpicker-alpha { + background-size: contain; +} +.colorpicker { + padding: 4px; + min-width: 130px; + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + z-index: 2500; +} +.colorpicker:before, +.colorpicker:after { + display: table; + content: ""; + line-height: 0; +} +.colorpicker:after { + clear: both; +} +.colorpicker:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 6px; +} +.colorpicker:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 7px; +} +.colorpicker div { + position: relative; +} +.colorpicker.colorpicker-with-alpha { + min-width: 140px; +} +.colorpicker.colorpicker-with-alpha .colorpicker-alpha { + display: block; +} +.colorpicker-color { + height: 10px; + margin-top: 5px; + clear: both; + background-image: url(static('colorpicker/alpha.png')); + background-position: 0 100%; +} +.colorpicker-color div { + height: 10px; +} +.colorpicker-selectors { + display: none; + height: 10px; + margin-top: 5px; + clear: both; +} +.colorpicker-selectors i { + cursor: pointer; + float: left; + height: 10px; + width: 10px; +} +.colorpicker-selectors i + i { + margin-left: 3px; +} +.colorpicker-element .input-group-addon i, +.colorpicker-element .add-on i { + display: inline-block; + cursor: pointer; + height: 16px; + vertical-align: text-top; + width: 16px; +} +.colorpicker.colorpicker-inline { + position: relative; + display: inline-block; + float: none; + z-index: auto; +} +.colorpicker.colorpicker-horizontal { + width: 110px; + min-width: 110px; + height: auto; +} +.colorpicker.colorpicker-horizontal .colorpicker-saturation { + margin-bottom: 4px; +} +.colorpicker.colorpicker-horizontal .colorpicker-color { + width: 100px; +} +.colorpicker.colorpicker-horizontal .colorpicker-hue, +.colorpicker.colorpicker-horizontal .colorpicker-alpha { + width: 100px; + height: 15px; + float: left; + cursor: col-resize; + margin-left: 0px; + margin-bottom: 4px; +} +.colorpicker.colorpicker-horizontal .colorpicker-hue i, +.colorpicker.colorpicker-horizontal .colorpicker-alpha i { + display: block; + height: 15px; + background: #ffffff; + position: absolute; + top: 0; + left: 0; + width: 1px; + border: none; + margin-top: 0px; +} +.colorpicker.colorpicker-horizontal .colorpicker-hue { + background-image: url(static('colorpicker/hue-horizontal.png')); +} +.colorpicker.colorpicker-horizontal .colorpicker-alpha { + background-image: url(static('colorpicker/alpha-horizontal.png')); +} +.colorpicker-right:before { + left: auto; + right: 6px; +} +.colorpicker-right:after { + left: auto; + right: 7px; +} +.colorpicker-no-arrow:before { + border-right: 0; + border-left: 0; +} +.colorpicker-no-arrow:after { + border-right: 0; + border-left: 0; +} +.colorpicker.colorpicker-visible, +.colorpicker-alpha.colorpicker-visible, +.colorpicker-saturation.colorpicker-visible, +.colorpicker-hue.colorpicker-visible, +.colorpicker-selectors.colorpicker-visible { + display: block; +} +.colorpicker.colorpicker-hidden, +.colorpicker-alpha.colorpicker-hidden, +.colorpicker-saturation.colorpicker-hidden, +.colorpicker-hue.colorpicker-hidden, +.colorpicker-selectors.colorpicker-hidden { + display: none; +} +.colorpicker-inline.colorpicker-visible { + display: inline-block; +} +/*# sourceMappingURL=bootstrap-colorpicker.css.map */ diff --git a/src/pretix/static/colorpicker/hue-horizontal.png b/src/pretix/static/colorpicker/hue-horizontal.png new file mode 100644 index 000000000..3dcd5946a Binary files /dev/null and b/src/pretix/static/colorpicker/hue-horizontal.png differ diff --git a/src/pretix/static/colorpicker/hue.png b/src/pretix/static/colorpicker/hue.png new file mode 100644 index 000000000..6f5ec2e50 Binary files /dev/null and b/src/pretix/static/colorpicker/hue.png differ diff --git a/src/pretix/static/colorpicker/saturation.png b/src/pretix/static/colorpicker/saturation.png new file mode 100644 index 000000000..170841cba Binary files /dev/null and b/src/pretix/static/colorpicker/saturation.png differ diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index f6fedf3f1..6892a5eff 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -187,6 +187,12 @@ $(function () { earlier_field.on("dp.change", update); }); + $(".colorpickerfield").colorpicker({ + format: 'hex', + align: 'left', + customClass: 'colorpicker-2x' + }); + $("input[data-checkbox-dependency]").each(function () { var dependent = $(this), dependency = $($(this).attr("data-checkbox-dependency")), diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index 9d140fef8..772e8aa50 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -11,6 +11,7 @@ @import "_orders.scss"; @import "_dashboard.scss"; @import "../../pretixbase/scss/webfont.scss"; +@import "../../colorpicker/bootstrap-colorpicker.scss"; footer { text-align: center; @@ -254,5 +255,20 @@ body.loading #wrapper { border: 0 !important; padding: 0 !important; display: block !important; +} -} \ No newline at end of file +.colorpicker-2x .colorpicker-saturation { + width: 200px; + height: 200px; +} + +.colorpicker-2x .colorpicker-hue, +.colorpicker-2x .colorpicker-alpha { + width: 30px; + height: 200px; +} + +.colorpicker-2x .colorpicker-color, +.colorpicker-2x .colorpicker-color div { + height: 30px; +}