/** * RightJS UI Colorpicker widget * * See http://rightjs.org/ui/colorpicker * * Copyright (C) 2010 Nikolay Nemshilov */ var Colorpicker = RightJS.Colorpicker = (function(document, Math, parseInt, RightJS) { /** * This module defines the basic widgets constructor * it creates an abstract proxy with the common functionality * which then we reuse and override in the actual widgets * * Copyright (C) 2010 Nikolay Nemshilov */ /** * The initialization files list * * Copyright (C) 2010 Nikolay Nemshilov */ var R = RightJS, $ = RightJS.$, $w = RightJS.$w, $$ = RightJS.$$, $E = RightJS.$E, $A = RightJS.$A, isArray = RightJS.isArray, Wrapper = RightJS.Wrapper, Element = RightJS.Element, Input = RightJS.Input; /** * The widget units constructor * * @param String tag-name or Object methods * @param Object methods * @return Widget wrapper */ function Widget(tag_name, methods) { if (!methods) { methods = tag_name; tag_name = 'DIV'; } /** * An Abstract Widget Unit * * Copyright (C) 2010 Nikolay Nemshilov */ var AbstractWidget = new RightJS.Wrapper(RightJS.Element.Wrappers[tag_name] || RightJS.Element, { /** * The common constructor * * @param Object options * @param String optional tag name * @return void */ initialize: function(key, options) { this.key = key; var args = [{'class': 'rui-' + key}]; // those two have different constructors if (!(this instanceof RightJS.Input || this instanceof RightJS.Form)) { args.unshift(tag_name); } this.$super.apply(this, args); if (RightJS.isString(options)) { options = RightJS.$(options); } // if the options is another element then // try to dynamically rewrap it with our widget if (options instanceof RightJS.Element) { this._ = options._; if ('$listeners' in options) { options.$listeners = options.$listeners; } options = {}; } this.setOptions(options, this); return this; }, // protected /** * Catches the options * * @param Object user-options * @param Element element with contextual options * @return void */ setOptions: function(options, element) { element = element || this; RightJS.Options.setOptions.call(this, RightJS.Object.merge(options, eval("("+( element.get('data-'+ this.key) || '{}' )+")")) ); return this; } }); /** * Creating the actual widget class * */ var Klass = new RightJS.Wrapper(AbstractWidget, methods); // creating the widget related shortcuts RightJS.Observer.createShortcuts(Klass.prototype, Klass.EVENTS || []); return Klass; } /** * A shared button unit. * NOTE: we use the DIV units instead of INPUTS * so those buttons didn't interfere with * the user's tab-index on his page * * Copyright (C) 2010 Nikolay Nemshilov */ var Button = new RightJS.Wrapper(RightJS.Element, { /** * Constructor * * @param String caption * @param Object options * @return void */ initialize: function(caption, options) { this.$super('div', options); this._.innerHTML = caption; this.addClass('rui-button'); this.on('selectstart', 'stopEvent'); }, /** * Disasbles the button * * @return Button this */ disable: function() { return this.addClass('rui-button-disabled'); }, /** * Enables the button * * @return Button this */ enable: function() { return this.removeClass('rui-button-disabled'); }, /** * Checks if the button is disabled * * @return Button this */ disabled: function() { return this.hasClass('rui-button-disabled'); }, /** * Checks if the button is enabled * * @return Button this */ enabled: function() { return !this.disabled(); }, /** * Overloading the method, so it fired the events * only when the button is active * * @return Button this */ fire: function() { if (this.enabled()) { this.$super.apply(this, arguments); } return this; } }); /** * A shared module that toggles a widget visibility status * in a uniformed way according to the options settings * * Copyright (C) 2010 Nikolay Nemshilov */ /** * The toggler's common functionality * * NOTE: this function getting called in the context * of a widget * * @param Element the element to toggle * @param event String 'show' or 'hide' the event name * @param String an optional fx-name * @param Object an optional fx-options hash * @return void */ function toggler(element, event, fx_name, fx_options) { if (RightJS.Fx) { if (fx_name === undefined) { fx_name = this.options.fxName; if (fx_options === undefined) { fx_options = { duration: this.options.fxDuration, onFinish: RightJS(this.fire).bind(this, event) }; // hide on double time if (event === 'hide') { fx_options.duration = (RightJS.Fx.Durations[fx_options.duration] || fx_options.duration) / 2; } } } } RightJS.Element.prototype[event].call(element, fx_name, fx_options); // manually trigger the event if no fx were specified if (!RightJS.Fx || !fx_name) { this.fire(event); } return this; } /** * Relatively positions the current element * against the specified one * * NOTE: this function is called in a context * of another element * * @param Element the target element * @param String position 'right' or 'bottom' * @param Boolean if `true` then the element size will be adjusted * @return void */ function re_position(element, where, resize) { var anchor = this.reAnchor || (this.reAnchor = new RightJS.Element('div', {'class': 'rui-re-anchor'})) .insert(this), pos = anchor.insertTo(element, 'after').position(), dims = element.dimensions(), target = this, border_top = parseInt(element.getStyle('borderTopWidth')), border_left = parseInt(element.getStyle('borderLeftWidth')), border_right = parseInt(element.getStyle('borderRightWidth')), border_bottom = parseInt(element.getStyle('borderBottomWidth')), top = dims.top - pos.y + border_top, left = dims.left - pos.x + border_left, width = dims.width - border_left - border_right, height = dims.height - border_top - border_bottom; // making the element to appear so we could read it's sizes target.setStyle('visibility:hidden').show(null); if (where === 'right') { left += width - target.size().x; } else { // bottom top += height; } target.moveTo(left, top); if (resize) { if (['left', 'right'].include(where)) { target.setHeight(height); } else { target.setWidth(width); } } // rolling the invisibility back target.setStyle('visibility:visible').hide(null); } /** * The actual shared module to be inserted in the widgets * * Copyright (C) 2010 Nikolay Nemshilov */ var Toggler = { /** * Shows the element * * @param String fx-name * @param Object fx-options * @return Element this */ show: function(fx_name, fx_options) { this.constructor.current = this; return toggler.call(this, this, 'show', fx_name, fx_options); }, /** * Hides the element * * @param String fx-name * @param Object fx-options * @return Element this */ hide: function(fx_name, fx_options) { this.constructor.current = null; return toggler.call(this, this, 'hide', fx_name, fx_options); }, /** * Toggles the widget at the given element * * @param Element the related element * @param String position right/bottom (bottom is the default) * @param Boolean marker if the element should be resized to the element size * @return Widget this */ showAt: function(element, where, resize) { this.hide(null).shownAt = element = RightJS.$(element); // moves this element at the given one re_position.call(this, element, where, resize); return this.show(); }, /** * Toggles the widget at the given element * * @param Element the related element * @param String position top/left/right/bottom (bottom is the default) * @param Boolean marker if the element should be resized to the element size * @return Widget this */ toggleAt: function(element, where, resize) { return this.hidden() ? this.showAt(element, where, resize) : this.hide(); } }; /** * A shared module that provides for the widgets an ability * to be assigned to an input element and work in pair with it * * NOTE: this module works in pair with the 'RePosition' module! * * Copyright (C) 2010 Nikolay Nemshilov */ var Assignable = { /** * Assigns the widget to serve the given input element * * Basically it puts the references of the current widget * to the input and trigger objects so they could be recognized * later, and it also synchronizes the changes between the input * element and the widget * * @param {Element} input field * @param {Element} optional trigger * @return Widget this */ assignTo: function(input, trigger) { input = RightJS.$(input); trigger = RightJS.$(trigger); if (trigger) { trigger[this.key] = this; trigger.assignedInput = input; } else { input[this.key] = this; } var on_change = RightJS(function() { if (this.visible() && (!this.showAt || this.shownAt === input)) { this.setValue(input.value()); } }).bind(this); input.on({ keyup: on_change, change: on_change }); this.onChange(function() { if (!this.showAt || this.shownAt === input) { input.setValue(this.getValue()); } }); return this; } }; /** * The basic file for Colorpicker * * Copyright (C) 2010 Nikolay Nemshilov */ var Colorpicker = new Widget({ include: [Toggler, Assignable], extend: { version: '2.0.0', EVENTS: $w('change show hide done'), Options: { format: 'hex', // hex or rgb update: null, // an element to update with the color text updateBg: null, // an element to update it's background color trigger: null, // a trigger element for the popup fxName: 'fade', // popup displaying fx fxDuration: 'short', cssRule: '*[data-colorpicker]' }, i18n: { Done: 'Done' }, // hides all the popup colorpickers on the page hideAll: function() { $$('div.rui-colorpicker').each(function(picker) { if (picker instanceof Colorpicker && !picker.inlined()) { picker.hide(); } }); } }, /** * basic constructor * * @param Object options */ initialize: function(options) { this .$super('colorpicker', options) .addClass('rui-panel') .insert([ this.field = new Field(), this.colors = new Colors(), this.controls = new Controls() ]) .on({ mousedown: this.startTrack, keyup: this.recalc, blur: this.update, focus: this.cancelTimer, done: this.done }); // hooking up the elements to update if (this.options.update) { this.assignTo(this.options.update, this.options.trigger); } if (this.options.updateBg) { this.updateBg(this.options.updateBg); } // setting up the initial values this.tint = R([1, 0, 0]); this.satur = 0; this.bright = 1; this.color = R([255, 255, 255]); this.recalc().update(); }, /** * Sets the color of the widget * * @param mixed value, Array or HEX or RGB value * @return Colorpicker this */ setValue: function(value) { var color = isArray(value) ? value : this.toColor(value); if (color && color.length === 3) { // normalizing the data color = color.map(function(value) { return this.bound(parseInt(''+value), 0, 255); }, this); this.color = color; this.color2tint().update(); // reupdating the popup-state a bit later when we have the sizes if (!this.colors.size().y) { this.update.bind(this).delay(20); } } return this; }, /** * Returns the value of the widget * formatted according to the options * * @param Boolean if you need a clean RGB values array * @return mixed value */ getValue: function(array) { return array ? this.color : this[this.options.format === 'rgb' ? 'toRgb' : 'toHex'](); }, /** * Assigns the colorpicer to automatically update * given element's background on changes * * @param mixed element reference * @return Colorpicker this */ updateBg: function(element_ref) { var element = $(element_ref); if (element) { this.onChange(R(function(color) { element._.style.backgroundColor = this.toRgb(); }).bind(this)); } return this; }, /** * Inlines the widget into the given element * * @param Element reference * @param String optional position * @return Colorpicker this */ insertTo: function(element, position) { return this .$super(element, position) .addClass('rui-colorpicker-inline'); }, /** * Checks if that's an inlined version of the widget * * @return Boolean check result */ inlined: function() { return this.hasClass('rui-colorpicker-inline'); }, /** * Finalizes the action * * @return Colorpicker this */ done: function() { if (!this.inlined()) { this.hide(); } return this; }, // protected // catching up the user options setOptions: function(user_options) { user_options = user_options || {}; this.$super(user_options, $(user_options.trigger || user_options.update)); }, // updates the preview and pointer positions update: function() { this.field._.style.backgroundColor = 'rgb('+ this.tint.map(function(c) { return Math.round(c*255); }) +')'; // updating the input fields var color = this.color, controls = this.controls; controls.preview._.style.backgroundColor = controls.display._.value = this.toHex(); controls.rDisplay._.value = color[0]; controls.gDisplay._.value = color[1]; controls.bDisplay._.value = color[2]; // adjusting the field pointer position var pointer = this.field.pointer._.style, field = this.field.size(), top = field.y - this.bright * field.y - 2, left = this.satur * field.x - 2; pointer.top = this.bound(top, 0, field.y - 5) + 'px'; pointer.left = this.bound(left, 0, field.x - 5) + 'px'; // adjusting the ting pointer position var tint = this.tint, position; field = this.colors.size(); if (tint[1] == 0) { // the red-blue section position = tint[0] == 1 ? tint[2] : (2 - tint[0]); } else if (tint[0] == 0) { // the blue-green section position = 2 + (tint[2] == 1 ? tint[1] : (2 - tint[2])); } else { // the green-red section position = 4 + (tint[1] == 1 ? tint[0] : (2 - tint[1])); } position = position / 6 * field.y; this.colors.pointer._.style.top = this.bound(position, 0, field.y - 4) + 'px'; // tracking the color change events if (this.prevColor !== ''+this.color) { this.fire('change', {value: this.color}); this.prevColor = ''+ this.color; } return this; }, // recalculates the state after the input field changes recalc: function(event) { if (event) { var field = event.target, value = field._.value, color = $A(this.color), changed=false; if (field === this.controls.display && /#\w{6}/.test(value)) { // using the hex values changed = color = this.toColor(value); } else if (/^\d+$/.test(value)) { // using the rgb values color[field._.cIndex] = value; changed = true; } if (changed) { this.setValue(color); } } else { this.tint2color(); } return this; }, // starts the mousemoves tracking startTrack: function(event) { this.stopTrack(); this.cancelTimer(); if (event.target === this.field.pointer) { event.target = this.field; } else if (event.target === this.colors.pointer) { event.target = this.colors; } if (event.target === this.field || event.target === this.colors) { event.stop(); Colorpicker.tracking = this; event.target.tracking = true; this.trackMove(event); // jumping over there } }, // stops tracking the mousemoves stopTrack: function() { Colorpicker.tracking = false; this.field.tracking = false; this.colors.tracking = false; }, // tracks the cursor moves over the fields trackMove: function(event) { var field, pos = event.position(), top, left; if (this.field.tracking) { field = this.field.dimensions(); } else if (this.colors.tracking) { field = this.colors.dimensions(); } if (field) { top = this.bound(pos.y - field.top, 0, field.height); left = this.bound(pos.x - field.left, 0, field.width); if (this.field.tracking) { this.satur = left / field.width; this.bright = 1 - top / field.height; } else if (this.colors.tracking) { // preventing it from jumping to the top if (top == field.height) { top = field.height - 0.1; } var step = field.height / 6, tint = this.tint = [0, 0, 0], stright = top % step / step, reverse = 1 - stright; if (top < step) { tint[0] = 1; tint[2] = stright; } else if (top < step * 2) { tint[0] = reverse; tint[2] = 1; } else if (top < step * 3) { tint[2] = 1; tint[1] = stright; } else if (top < step * 4) { tint[2] = reverse; tint[1] = 1; } else if (top < step * 5) { tint[1] = 1; tint[0] = stright; } else { tint[1] = reverse; tint[0] = 1; } } this.recalc().update(); } }, cancelTimer: function(event) { R(function() { // IE has a lack of sync in here if (this._hide_delay) { this._hide_delay.cancel(); this._hide_delay = null; } }).bind(this).delay(10); } }); /** * The colors field element * * Copyright (C) 2010 */ var Field = new Wrapper(Element, { initialize: function(options) { this.$super('div', {'class': 'field'}); this.insert(this.pointer = $E('div', {'class': 'pointer'})); } }); /** * The tint picker block * * Copyright (C) 2010 Nikolay Nemshilov */ var Colors = new Wrapper(Element, { initialize: function() { this.$super('div', {'class': 'colors'}); this.insert(this.pointer = $E('div', {'class': 'pointer'})); } }); /** * The controls block unit * * Copyright (C) 2010 Nikolay Nemshilov */ var Controls = new Wrapper(Element, { initialize: function() { this.$super('div', {'class': 'controls'}); this.insert([ this.preview = $E('div', {'class': 'preview', 'html': ' '}), this.display = $E('input', {'type': 'text', 'class': 'display', maxlength: 7}), $E('div', {'class': 'rgb-display'}).insert([ $E('div').insert([$E('label', {html: 'R:'}), this.rDisplay = $E('input', {maxlength: 3, cIndex: 0})]), $E('div').insert([$E('label', {html: 'G:'}), this.gDisplay = $E('input', {maxlength: 3, cIndex: 1})]), $E('div').insert([$E('label', {html: 'B:'}), this.bDisplay = $E('input', {maxlength: 3, cIndex: 2})]) ]), this.button = new Button(Colorpicker.i18n.Done).onClick('fire', 'done') ]); } }); /** * This module contains various caluculations logic for * the Colorpicker widget * * Copyright (C) 2010 Nikolay Nemshilov */ Colorpicker.include({ /** * Converts the color to a RGB string value * * @param Array optional color * @return String RGB value */ toRgb: function(color) { return 'rgb('+ this.color.join(',') +')'; }, /** * Converts the color to a HEX string value * * @param Array optional color * @return String HEX value */ toHex: function(color) { return '#'+ this.color.map(function(c) { return (c < 16 ? '0' : '') + c.toString(16); }).join(''); }, /** * Converts a string value into an Array of color * * @param String value * @return Array of color or null */ toColor: function(in_value) { var value = in_value.toLowerCase(), match; if ((match = /rgb\((\d+),(\d+),(\d+)\)/.exec(value))) { return [match[1], match[2], match[3]].map(parseInt); } else if (/#[\da-f]+/.test(value)) { // converting the shortified hex in to the full-length version if ((match = /^#([\da-f])([\da-f])([\da-f])$/.exec(value))) { value = '#'+match[1]+match[1]+match[2]+match[2]+match[3]+match[3]; } if ((match = /#([\da-f]{2})([\da-f]{2})([\da-f]{2})/.exec(value))) { return [match[1], match[2], match[3]].map(function(n) { return parseInt(n, 16); }); } } }, /** * converts color into the tint, saturation and brightness values * * @return Colorpicker this */ color2tint: function() { var color = $A(this.color).sort(function(a,b) { return a-b; }), min = color[0], max = color[2]; this.bright = max / 255; this.satur = 1 - min / (max || 1); this.tint.each(function(value, i) { this.tint[i] = ((!min && !max) || min == max) ? i == 0 ? 1 : 0 : (this.color[i] - min) / (max - min); return this.tint[i]; }, this); return this; }, /** * Converts tint, saturation and brightness into the actual RGB color * * @return Colorpicker this */ tint2color: function() { var tint = this.tint, color = this.color; for (var i=0; i < 3; i++) { color[i] = 1 + this.satur * (tint[i] - 1); color[i] = Math.round(255 * color[i] * this.bright); } return this; }, /** * bounds the value to the given limits * * @param {Number} value * @param {Number} min value * @param {Number} max value * @return {Number} the value in bounds */ bound: function(in_value, min, max) { var value = in_value; if (min < max) { value = value < min ? min : value > max ? max : value; } else { if (value > max) { value = max; } if (value < min) { value = min; } } return value; } }); /** * The document level hooks for colorpicker * * Copyright (C) 2010 Nikolay Nemshilov */ $(document).on({ mouseup: function() { if (Colorpicker.tracking) { Colorpicker.tracking.stopTrack(); } }, mousemove: function(event) { if (Colorpicker.tracking) { Colorpicker.tracking.trackMove(event); } }, focus: function(event) { var target = event.target instanceof Input ? event.target : null; Colorpicker.hideAll(); if (target && (target.colorpicker || target.match(Colorpicker.Options.cssRule))) { (target.colorpicker || new Colorpicker({update: target})) .setValue(target.value()).showAt(target); } }, blur: function(event) { var target = event.target, colorpicker = target.colorpicker; if (colorpicker) { // we use the delay so it didn't get hidden when the user clicks the calendar itself colorpicker._hide_delay = R(function() { colorpicker.hide(); }).delay(200); } }, click: function(event) { var target = (event.target instanceof Element) ? event.target : null; if (target && (target.colorpicker || target.match(Colorpicker.Options.cssRule))) { if (!(target instanceof Input)) { event.stop(); (target.colorpicker || new Colorpicker({trigger: target})) .hide(null).toggleAt(target.assignedInput); } } else if (!event.find('div.rui-colorpicker')){ Colorpicker.hideAll(); } }, keydown: function(event) { var colorpicker = Colorpicker.current, name = ({ 27: 'hide', // Escape 13: 'done' // Enter })[event.keyCode]; if (name && colorpicker && colorpicker.visible()) { event.stop(); colorpicker[name](); } } }); document.write(""); return Colorpicker; })(document, Math, parseInt, RightJS);