# Yahoo! UI Library

### colorpicker 2.8.0r4

Yahoo! UI Library > colorpicker > ColorPicker.js (source view)
Search:
Filters Show Private Show Protected Show Deprecated

    /**
     * The colorpicker module provides a widget for selecting colors
     * @module colorpicker
     * @requires yahoo, dom, event, element, slider
     */
    (function() {

        var _pickercount = 0,
            util   = YAHOO.util,
            lang   = YAHOO.lang,
            Slider = YAHOO.widget.Slider,
            Color  = util.Color,
            Dom    = util.Dom,
            Event  = util.Event,
            sub    = lang.substitute,

            b = "yui-picker";

        /**
         * A widget to select colors
         * @namespace YAHOO.widget
         * @class YAHOO.widget.ColorPicker
         * @extends YAHOO.util.Element
         * @constructor
         * @param {HTMLElement | String | Object} el(optional) The html
         * element that represents the colorpicker, or the attribute object to use.
         * An element will be created if none provided.
         * @param {Object} attr (optional) A key map of the colorpicker's
         * initial attributes.  Ignored if first arg is attributes object.
         */
        function ColorPicker(el, attr) {
            _pickercount = _pickercount + 1;
            this.logger = new YAHOO.widget.LogWriter("ColorPicker");
            attr = attr || {};
            if (arguments.length === 1 && !YAHOO.lang.isString(el) && !el.nodeName) {
                attr = el; // treat first arg as attr object
                el = attr.element || null;
            }

            if (!el && !attr.element) { // create if we dont have one
                this.logger.log("creating host element");
                el = this._createHostElement(attr);
            }

        	ColorPicker.superclass.constructor.call(this, el, attr);

            this.initPicker();
        }

        YAHOO.extend(ColorPicker, YAHOO.util.Element, {

            /**
             * The element ids used by this control
             * @property ID
             * @final
             */
            ID : {

                /**
                 * The id for the "red" form field
                 * @property ID.R
                 * @type String
                 * @final
                 * @default yui-picker-r
                 */
                R: b + "-r",

                /**
                 * The id for the "red" hex pair output
                 * @property ID.R_HEX
                 * @type String
                 * @final
                 * @default yui-picker-rhex
                 */
                R_HEX: b + "-rhex",

                /**
                 * The id for the "green" form field
                 * @property ID.G
                 * @type String
                 * @final
                 * @default yui-picker-g
                 */
                G: b + "-g",

                /**
                 * The id for the "green" hex pair output
                 * @property ID.G_HEX
                 * @type String
                 * @final
                 * @default yui-picker-ghex
                 */
                G_HEX: b + "-ghex",

                /**
                 * The id for the "blue" form field
                 * @property ID.B
                 * @type String
                 * @final
                 * @default yui-picker-b
                 */
                B: b + "-b",

                /**
                 * The id for the "blue" hex pair output
                 * @property ID.B_HEX
                 * @type String
                 * @final
                 * @default yui-picker-bhex
                 */
                B_HEX: b + "-bhex",

                /**
                 * The id for the "hue" form field
                 * @property ID.H
                 * @type String
                 * @final
                 * @default yui-picker-h
                 */
                H: b + "-h",

                /**
                 * The id for the "saturation" form field
                 * @property ID.S
                 * @type String
                 * @final
                 * @default yui-picker-s
                 */
                S: b + "-s",

                /**
                 * The id for the "value" form field
                 * @property ID.V
                 * @type String
                 * @final
                 * @default yui-picker-v
                 */
                V: b + "-v",

                /**
                 * The id for the picker region slider
                 * @property ID.PICKER_BG
                 * @type String
                 * @final
                 * @default yui-picker-bg
                 */
                PICKER_BG:      b + "-bg",

                /**
                 * The id for the picker region thumb
                 * @property ID.PICKER_THUMB
                 * @type String
                 * @final
                 * @default yui-picker-thumb
                 */
                PICKER_THUMB:   b + "-thumb",

                /**
                 * The id for the hue slider
                 * @property ID.HUE_BG
                 * @type String
                 * @final
                 * @default yui-picker-hue-bg
                 */
                HUE_BG:         b + "-hue-bg",

                /**
                 * The id for the hue thumb
                 * @property ID.HUE_THUMB
                 * @type String
                 * @final
                 * @default yui-picker-hue-thumb
                 */
                HUE_THUMB:      b + "-hue-thumb",

                /**
                 * The id for the hex value form field
                 * @property ID.HEX
                 * @type String
                 * @final
                 * @default yui-picker-hex
                 */
                HEX:            b + "-hex",

                /**
                 * The id for the color swatch
                 * @property ID.SWATCH
                 * @type String
                 * @final
                 * @default yui-picker-swatch
                 */
                SWATCH:         b + "-swatch",

                /**
                 * The id for the websafe color swatch
                 * @property ID.WEBSAFE_SWATCH
                 * @type String
                 * @final
                 * @default yui-picker-websafe-swatch
                 */
                WEBSAFE_SWATCH: b + "-websafe-swatch",

                /**
                 * The id for the control details
                 * @property ID.CONTROLS
                 * @final
                 * @default yui-picker-controls
                 */
                CONTROLS: b + "-controls",

                /**
                 * The id for the rgb controls
                 * @property ID.RGB_CONTROLS
                 * @final
                 * @default yui-picker-rgb-controls
                 */
                RGB_CONTROLS: b + "-rgb-controls",

                /**
                 * The id for the hsv controls
                 * @property ID.HSV_CONTROLS
                 * @final
                 * @default yui-picker-hsv-controls
                 */
                HSV_CONTROLS: b + "-hsv-controls",

                /**
                 * The id for the hsv controls
                 * @property ID.HEX_CONTROLS
                 * @final
                 * @default yui-picker-hex-controls
                 */
                HEX_CONTROLS: b + "-hex-controls",

                /**
                 * The id for the hex summary
                 * @property ID.HEX_SUMMARY
                 * @final
                 * @default yui-picker-hex-summary
                 */
                HEX_SUMMARY: b + "-hex-summary",

                /**
                 * The id for the controls section header
                 * @property ID.CONTROLS_LABEL
                 * @final
                 * @default yui-picker-controls-label
                 */
                CONTROLS_LABEL: b + "-controls-label"
            },

            /**
             * Constants for any script-generated messages.  The values here
             * are the default messages.  They can be updated by providing
             * the complete list to the constructor for the "txt" attribute.
             * @property TXT
             * @final
             */
            TXT : {
                ILLEGAL_HEX: "Illegal hex value entered",
                SHOW_CONTROLS: "Show color details",
                HIDE_CONTROLS: "Hide color details",
                CURRENT_COLOR: "Currently selected color: {rgb}",
                CLOSEST_WEBSAFE: "Closest websafe color: {rgb}. Click to select.",
                R: "R",
                G: "G",
                B: "B",
                H: "H",
                S: "S",
                V: "V",
                HEX: "#",
                DEG: "\u00B0",
                PERCENT: "%"
            },

            /**
             * Constants for the default image locations for img tags that are
             * generated by the control.  They can be modified by passing the
             * complete list to the contructor for the "images" attribute
             * @property IMAGE
             * @final
             */
            IMAGE : {
                PICKER_THUMB: "../../build/colorpicker/assets/picker_thumb.png",
                HUE_THUMB: "../../build/colorpicker/assets/hue_thumb.png"
            },

            /**
             * Constants for the control's default default values
             * @property DEFAULT
             * @final
             */
            DEFAULT : {
                PICKER_SIZE: 180
            },

            /**
             * Constants for the control's configuration attributes
             * @property OPT
             * @final
             */
            OPT : {
                HUE         : "hue",
                SATURATION  : "saturation",
                VALUE       : "value",
                RED     : "red",
                GREEN   : "green",
                BLUE    : "blue",
                HSV     : "hsv",
                RGB     : "rgb",
                WEBSAFE : "websafe",
                HEX     : "hex",
                PICKER_SIZE       : "pickersize",
                SHOW_CONTROLS     : "showcontrols",
                SHOW_RGB_CONTROLS : "showrgbcontrols",
                SHOW_HSV_CONTROLS : "showhsvcontrols",
                SHOW_HEX_CONTROLS : "showhexcontrols",
                SHOW_HEX_SUMMARY  : "showhexsummary",
                SHOW_WEBSAFE      : "showwebsafe",
                CONTAINER         : "container",
                IDS      : "ids",
                ELEMENTS : "elements",
                TXT      : "txt",
                IMAGES   : "images",
                ANIMATE  : "animate"
            },

            /**
             * Flag to allow individual UI updates to forego animation if available.
             * True during construction for initial thumb placement.  Set to false
             * after that.
             *
             * @property skipAnim
             * @type Boolean
             * @default true
             */
            skipAnim : true,

            /**
             * Creates the host element if it doesn't exist
             * @method _createHostElement
             * @protected
             */
            _createHostElement : function () {
                var el = document.createElement('div');

                if (this.CSS.BASE) {
                    el.className = this.CSS.BASE;
                }

                return el;
            },

            /**
             * Moves the hue slider into the position dictated by the current state
             * of the control
             * @method _updateHueSlider
             * @protected
             */
            _updateHueSlider : function() {
                var size = this.get(this.OPT.PICKER_SIZE),
                    h = this.get(this.OPT.HUE);

                h = size - Math.round(h / 360 * size);

                // 0 is at the top and bottom of the hue slider.  Always go to
                // the top so we don't end up sending the thumb to the bottom
                // when the value didn't actually change (e.g., a conversion
                // produced 360 instead of 0 and the value was already 0).
                if (h === size) {
                    h = 0;
                }
                this.logger.log("Hue slider is being set to " + h);

                this.hueSlider.setValue(h, this.skipAnim);
            },

            /**
             * Moves the picker slider into the position dictated by the current state
             * of the control
             * @method _updatePickerSlider
             * @protected
             */
            _updatePickerSlider : function() {
                var size = this.get(this.OPT.PICKER_SIZE),
                    s = this.get(this.OPT.SATURATION),
                    v = this.get(this.OPT.VALUE);

                s = Math.round(s * size / 100);
                v = Math.round(size - (v * size / 100));

                this.logger.log("Setting picker slider to " + [s, v]);

                this.pickerSlider.setRegionValue(s, v, this.skipAnim);
            },

            /**
             * Moves the sliders into the position dictated by the current state
             * of the control
             * @method _updateSliders
             * @protected
             */
            _updateSliders : function() {
                this._updateHueSlider();
                this._updatePickerSlider();
            },

            /**
             * Sets the control to the specified rgb value and
             * moves the sliders to the proper positions
             * @method setValue
             * @param rgb {[int, int, int]} the rgb value
             * @param silent {boolean} whether or not to fire the change event
             */
            setValue : function(rgb, silent) {
                silent = (silent) || false;
                this.set(this.OPT.RGB, rgb, silent);
                this._updateSliders();
            },

            /**
             * The hue slider
             * @property hueSlider
             * @type YAHOO.widget.Slider
             */
            hueSlider : null,

            /**
             * The picker region
             * @property pickerSlider
             * @type YAHOO.widget.Slider
             */
            pickerSlider : null,

            /**
             * Translates the slider value into hue, int[0,359]
             * @method _getH
             * @protected
             * @return {int} the hue from 0 to 359
             */
            _getH : function() {
                var size = this.get(this.OPT.PICKER_SIZE),
                    h = (size - this.hueSlider.getValue()) / size;
                h = Math.round(h*360);
                return (h === 360) ? 0 : h;
            },

            /**
             * Translates the slider value into saturation, int[0,1], left to right
             * @method _getS
             * @protected
             * @return {int} the saturation from 0 to 1
             */
            _getS : function() {
                return this.pickerSlider.getXValue() / this.get(this.OPT.PICKER_SIZE);
            },

            /**
             * Translates the slider value into value/brightness, int[0,1], top
             * to bottom
             * @method _getV
             * @protected
             * @return {int} the value from 0 to 1
             */
            _getV : function() {
                var size = this.get(this.OPT.PICKER_SIZE);
                return (size - this.pickerSlider.getYValue()) / size;
            },

            /**
             * Updates the background of the swatch with the current rbg value.
             * Also updates the websafe swatch to the closest websafe color
             * @method _updateSwatch
             * @protected
             */
            _updateSwatch : function() {
                var rgb = this.get(this.OPT.RGB),
                    websafe = this.get(this.OPT.WEBSAFE),
                    el = this.getElement(this.ID.SWATCH),
                    color = rgb.join(","),
                    txt = this.get(this.OPT.TXT);

                Dom.setStyle(el, "background-color", "rgb(" + color  + ")");
                el.title = sub(txt.CURRENT_COLOR, {
                        "rgb": "#" + this.get(this.OPT.HEX)
                    });

                el = this.getElement(this.ID.WEBSAFE_SWATCH);
                color = websafe.join(",");

                Dom.setStyle(el, "background-color", "rgb(" + color + ")");
                el.title = sub(txt.CLOSEST_WEBSAFE, {
                        "rgb": "#" + Color.rgb2hex(websafe)
                    });

            },

            /**
             * Reads the sliders and converts the values to RGB, updating the
             * internal state for all the individual form fields
             * @method _getValuesFromSliders
             * @protected
             */
            _getValuesFromSliders : function() {
                this.logger.log("hsv " + [this._getH(),this._getS(),this._getV()]);
                this.set(this.OPT.RGB, Color.hsv2rgb(this._getH(), this._getS(), this._getV()));
            },

            /**
             * Updates the form field controls with the state data contained
             * in the control.
             * @method _updateFormFields
             * @protected
             */
            _updateFormFields : function() {
                this.getElement(this.ID.H).value = this.get(this.OPT.HUE);
                this.getElement(this.ID.S).value = this.get(this.OPT.SATURATION);
                this.getElement(this.ID.V).value = this.get(this.OPT.VALUE);
                this.getElement(this.ID.R).value = this.get(this.OPT.RED);
                this.getElement(this.ID.R_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.RED));
                this.getElement(this.ID.G).value = this.get(this.OPT.GREEN);
                this.getElement(this.ID.G_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.GREEN));
                this.getElement(this.ID.B).value = this.get(this.OPT.BLUE);
                this.getElement(this.ID.B_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.BLUE));
                this.getElement(this.ID.HEX).value = this.get(this.OPT.HEX);
            },

            /**
             * Event handler for the hue slider.
             * @method _onHueSliderChange
             * @param newOffset {int} pixels from the start position
             * @protected
             */
            _onHueSliderChange : function(newOffset) {
                this.logger.log("hue update: " + newOffset , "warn");

                var h        = this._getH(),
                    rgb      = Color.hsv2rgb(h, 1, 1),
                    styleDef = "rgb(" + rgb.join(",") + ")";

                this.set(this.OPT.HUE, h, true);

                // set picker background to the hue
                Dom.setStyle(this.getElement(this.ID.PICKER_BG), "background-color", styleDef);

                if (this.hueSlider.valueChangeSource !== Slider.SOURCE_SET_VALUE) {
                    this._getValuesFromSliders();
                }

                this._updateFormFields();
                this._updateSwatch();
            },

            /**
             * Event handler for the picker slider, which controls the
             * saturation and value/brightness.
             * @method _onPickerSliderChange
             * @param newOffset {{x: int, y: int}} x/y pixels from the start position
             * @protected
             */
            _onPickerSliderChange : function(newOffset) {
                this.logger.log(sub("picker update [{x}, {y}]", newOffset));

                var s=this._getS(), v=this._getV();
                this.set(this.OPT.SATURATION, Math.round(s*100), true);
                this.set(this.OPT.VALUE, Math.round(v*100), true);

                if (this.pickerSlider.valueChangeSource !== Slider.SOURCE_SET_VALUE) {
                    this._getValuesFromSliders();
                }

                this._updateFormFields();
                this._updateSwatch();
            },

            /**
             * Key map to well-known commands for txt field input
             * @method _getCommand
             * @param e {Event} the keypress or keydown event
             * @return {int} a command code
             * <ul>
             * <li>0 = not a number, letter in range, or special key</li>
             * <li>1 = number</li>
             * <li>2 = a-fA-F</li>
             * <li>3 = increment (up arrow)</li>
             * <li>4 = decrement (down arrow)</li>
             * <li>5 = special key (tab, delete, return, escape, left, right)</li>
             * <li>6 = return</li>
             * </ul>
             * @protected
             */
            _getCommand : function(e) {
                var c = Event.getCharCode(e);

                //alert(Event.getCharCode(e) + ", " + e.keyCode + ", " + e.charCode);

                // special keys
                if (c === 38) { // up arrow
                    return 3;
                } else if (c === 13) { // return
                    return 6;
                } else if (c === 40) { // down array
                    return 4;
                } else if (c >= 48 && c<=57) { // 0-9
                    return 1;
                } else if (c >= 97 && c<=102) { // a-f
                    return 2;
                } else if (c >= 65 && c<=70) { // A-F
                    return 2;
                //} else if ("8, 9, 13, 27, 37, 39".indexOf(c) > -1 ||
                //              (c >= 112 && c <=123)) { // including F-keys
                // tab, delete, return, escape, left, right or ctrl/meta sequences
                } else if ("8, 9, 13, 27, 37, 39".indexOf(c) > -1 ||
                           e.ctrlKey || e.metaKey) { // special chars
                    return 5;
                } else { // something we probably don't want
                    return 0;
                }
            },

            /**
             * Use the value of the text field to update the control
             * @method _useFieldValue
             * @param e {Event} an event
             * @param el {HTMLElement} the field
             * @param prop {string} the key to the linked property
             * @protected
             */
            _useFieldValue : function(e, el, prop) {
                var val = el.value;

                if (prop !== this.OPT.HEX) {
                    val = parseInt(val, 10);
                }

                if (val !== this.get(prop)) {
                    this.set(prop, val);
                }
            },

            /**
             * Handle keypress on one of the rgb or hsv fields.
             * @method _rgbFieldKeypress
             * @param e {Event} the keypress event
             * @param el {HTMLElement} the field
             * @param prop {string} the key to the linked property
             * @protected
             */
            _rgbFieldKeypress : function(e, el, prop) {
                var command = this._getCommand(e),
                    inc = (e.shiftKey) ? 10 : 1;
                switch (command) {
                    case 6: // return, update the value
                        this._useFieldValue.apply(this, arguments);
                        break;

                    case 3: // up arrow, increment
                        this.set(prop, Math.min(this.get(prop)+inc, 255));
                        this._updateFormFields();
                        //Event.stopEvent(e);
                        break;
                    case 4: // down arrow, decrement
                        this.set(prop, Math.max(this.get(prop)-inc, 0));
                        this._updateFormFields();
                        //Event.stopEvent(e);
                        break;

                    default:
                }

            },

            /**
             * Handle keydown on the hex field
             * @method _hexFieldKeypress
             * @param e {Event} the keypress event
             * @param el {HTMLElement} the field
             * @param prop {string} the key to the linked property
             * @protected
             */
            _hexFieldKeypress : function(e, el, prop) {
                var command = this._getCommand(e);
                if (command === 6) { // return, update the value
                    this._useFieldValue.apply(this, arguments);
                }
            },

            /**
             * Allows numbers and special chars, and by default allows a-f.
             * Used for the hex field keypress handler.
             * @method _hexOnly
             * @param e {Event} the event
             * @param numbersOnly omits a-f if set to true
             * @protected
             * @return {boolean} false if we are canceling the event
             */
            _hexOnly : function(e, numbersOnly) {
                var command = this._getCommand(e);
                switch (command) {
                    case 6: // return
                    case 5: // special char
                    case 1: // number
                        break;
                    case 2: // hex char (a-f)
                        if (numbersOnly !== true) {
                            break;
                        }

                        // fallthrough is intentional

                    default: // prevent alpha and punctuation
                        Event.stopEvent(e);
                        return false;
                }
            },

            /**
             * Allows numbers and special chars only.  Used for the
             * rgb and hsv fields keypress handler.
             * @method _numbersOnly
             * @param e {Event} the event
             * @protected
             * @return {boolean} false if we are canceling the event
             */
            _numbersOnly : function(e) {
                return this._hexOnly(e, true);
            },

            /**
             * Returns the element reference that is saved.  The id can be either
             * the element id, or the key for this id in the "id" config attribute.
             * For instance, the host element id can be obtained by passing its
             * id (default: "yui_picker") or by its key "YUI_PICKER".
             * @param id {string} the element id, or key
             * @return {HTMLElement} a reference to the element
             */
            getElement : function(id) {
                return this.get(this.OPT.ELEMENTS)[this.get(this.OPT.IDS)[id]];
            },

            _createElements : function() {
                this.logger.log("Building markup");
                var el, child, img, fld, p,
                    ids = this.get(this.OPT.IDS),
                    txt = this.get(this.OPT.TXT),
                    images = this.get(this.OPT.IMAGES),
                    Elem = function(type, o) {
                        var n = document.createElement(type);
                        if (o) {
                            lang.augmentObject(n, o, true);
                        }
                        return n;
                    },
                    RGBElem = function(type, obj) {
                        var o = lang.merge({
                                //type: "txt",
                                autocomplete: "off",
                                value: "0",
                                size: 3,
                                maxlength: 3
                            }, obj);

                        o.name = o.id;
                        return new Elem(type, o);
                    };

                p = this.get("element");

                // Picker slider (S and V) ---------------------------------------------

                el = new Elem("div", {
                    id: ids[this.ID.PICKER_BG],
                    className: "yui-picker-bg",
                    tabIndex: -1,
                    hideFocus: true
                });

                child = new Elem("div", {
                    id: ids[this.ID.PICKER_THUMB],
                    className: "yui-picker-thumb"
                });

                img = new Elem("img", {
                    src: images.PICKER_THUMB
                });

                child.appendChild(img);
                el.appendChild(child);
                p.appendChild(el);

                // Hue slider ---------------------------------------------
                el = new Elem("div", {
                    id: ids[this.ID.HUE_BG],
                    className: "yui-picker-hue-bg",
                    tabIndex: -1,
                    hideFocus: true
                });

                child = new Elem("div", {
                    id: ids[this.ID.HUE_THUMB],
                    className: "yui-picker-hue-thumb"
                });

                img = new Elem("img", {
                    src: images.HUE_THUMB
                });

                child.appendChild(img);
                el.appendChild(child);
                p.appendChild(el);

                // controls ---------------------------------------------

                el = new Elem("div", {
                    id: ids[this.ID.CONTROLS],
                    className: "yui-picker-controls"
                });

                p.appendChild(el);
                p = el;

                    // controls header
                    el = new Elem("div", {
                        className: "hd"
                    });

                    child = new Elem("a", {
                        id: ids[this.ID.CONTROLS_LABEL],
                        //className: "yui-picker-controls-label",
                        href: "#"
                    });
                    el.appendChild(child);
                    p.appendChild(el);

                    // bd
                    el = new Elem("div", {
                        className: "bd"
                    });

                    p.appendChild(el);
                    p = el;

                        // rgb
                        el = new Elem("ul", {
                            id: ids[this.ID.RGB_CONTROLS],
                            className: "yui-picker-rgb-controls"
                        });

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.R + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.R],
                            className: "yui-picker-r"
                        });

                        child.appendChild(fld);
                        el.appendChild(child);

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.G + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.G],
                            className: "yui-picker-g"
                        });

                        child.appendChild(fld);
                        el.appendChild(child);

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.B + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.B],
                            className: "yui-picker-b"
                        });

                        child.appendChild(fld);
                        el.appendChild(child);

                        p.appendChild(el);

                        // hsv
                        el = new Elem("ul", {
                            id: ids[this.ID.HSV_CONTROLS],
                            className: "yui-picker-hsv-controls"
                        });

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.H + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.H],
                            className: "yui-picker-h"
                        });

                        child.appendChild(fld);
                        child.appendChild(document.createTextNode(" " + txt.DEG));

                        el.appendChild(child);

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.S + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.S],
                            className: "yui-picker-s"
                        });

                        child.appendChild(fld);
                        child.appendChild(document.createTextNode(" " + txt.PERCENT));

                        el.appendChild(child);

                        child = new Elem("li");
                        child.appendChild(document.createTextNode(txt.V + " "));

                        fld = new RGBElem("input", {
                            id: ids[this.ID.V],
                            className: "yui-picker-v"
                        });

                        child.appendChild(fld);
                        child.appendChild(document.createTextNode(" " + txt.PERCENT));

                        el.appendChild(child);
                        p.appendChild(el);

                        // hex summary

                        el = new Elem("ul", {
                            id: ids[this.ID.HEX_SUMMARY],
                            className: "yui-picker-hex_summary"
                        });

                        child = new Elem("li", {
                            id: ids[this.ID.R_HEX]
                        });
                        el.appendChild(child);

                        child = new Elem("li", {
                            id: ids[this.ID.G_HEX]
                        });
                        el.appendChild(child);

                        child = new Elem("li", {
                            id: ids[this.ID.B_HEX]
                        });
                        el.appendChild(child);
                        p.appendChild(el);

                        // hex field
                        el = new Elem("div", {
                            id: ids[this.ID.HEX_CONTROLS],
                            className: "yui-picker-hex-controls"
                        });
                        el.appendChild(document.createTextNode(txt.HEX + " "));

                        child = new RGBElem("input", {
                            id: ids[this.ID.HEX],
                            className: "yui-picker-hex",
                            size: 6,
                            maxlength: 6
                        });

                        el.appendChild(child);
                        p.appendChild(el);

                        p = this.get("element");

                        // swatch
                        el = new Elem("div", {
                            id: ids[this.ID.SWATCH],
                            className: "yui-picker-swatch"
                        });

                        p.appendChild(el);

                        // websafe swatch
                        el = new Elem("div", {
                            id: ids[this.ID.WEBSAFE_SWATCH],
                            className: "yui-picker-websafe-swatch"
                        });

                        p.appendChild(el);

            },

            _attachRGBHSV : function(id, config) {
                Event.on(this.getElement(id), "keydown", function(e, me) {
                        me._rgbFieldKeypress(e, this, config);
                    }, this);
                Event.on(this.getElement(id), "keypress", this._numbersOnly, this, true);
                Event.on(this.getElement(id), "blur", function(e, me) {
                        me._useFieldValue(e, this, config);
                    }, this);
            },

            /**
             * Updates the rgb attribute with the current state of the r,g,b
             * fields.  This is invoked from change listeners on these
             * attributes to facilitate updating these values from the
             * individual form fields
             * @method _updateRGB
             * @protected
             */
            _updateRGB : function() {
                var rgb = [this.get(this.OPT.RED),
                           this.get(this.OPT.GREEN),
                           this.get(this.OPT.BLUE)];

                this.logger.log("RGB value set to " + rgb);
                this.set(this.OPT.RGB, rgb);

                this._updateSliders();
            },

            /**
             * Creates any missing DOM structure.
             *
             * @method _initElements
             * @protected
             */
            _initElements : function () {
                // bind all of our elements
                var o=this.OPT,
                    ids = this.get(o.IDS),
                    els = this.get(o.ELEMENTS),
                          i, el, id;

                // Add the default value as a key for each element for easier lookup
                for (i in this.ID) {
                    if (lang.hasOwnProperty(this.ID, i)) {
                        ids[this.ID[i]] = ids[i];
                    }
                }

                // Check for picker element, if not there, create all of them
                el = Dom.get(ids[this.ID.PICKER_BG]);
                if (!el) {
                    this._createElements();
                } else {
                    this.logger.log("Using pre-existing markup");
                }

                for (i in ids) {
                    if (lang.hasOwnProperty(ids, i)) {
                        // look for element
                        el = Dom.get(ids[i]);

                        // generate an id if the implementer passed in an element reference,
                        // and the element did not have an id already
                        id = Dom.generateId(el);

                        // update the id in case we generated the id
                        ids[i] = id; // key is WEBSAFE_SWATCH
                        ids[ids[i]] = id; // key is websafe_swatch

                        // store the dom ref
                        els[id] = el;
                    }
                }

            },

            /**
             * Sets the initial state of the sliders
             * @method initPicker
             */
            initPicker : function () {
                this._initSliders();
                this._bindUI();
                this.syncUI(true);
            },

            /**
             * Creates the Hue and Value/Saturation Sliders.
             *
             * @method _initSliders
             * @protected
             */
            _initSliders : function () {
                var ID = this.ID,
                    size = this.get(this.OPT.PICKER_SIZE);

                this.logger.log("picker size" + size);

                this.hueSlider = Slider.getVertSlider(
                    this.getElement(ID.HUE_BG),
                    this.getElement(ID.HUE_THUMB), 0, size);

                this.pickerSlider = Slider.getSliderRegion(
                    this.getElement(ID.PICKER_BG),
                    this.getElement(ID.PICKER_THUMB), 0, size, 0, size);

                // Apply animate attribute configuration
                this.set(this.OPT.ANIMATE, this.get(this.OPT.ANIMATE));
            },

            /**
             * Adds event listeners to Sliders and UI elements.  Wires everything
             * up.
             *
             * @method _bindUI
             * @protected
             */
            _bindUI : function () {
                var ID = this.ID,
                    O  = this.OPT;

                this.hueSlider.subscribe("change",
                    this._onHueSliderChange, this, true);
                this.pickerSlider.subscribe("change",
                    this._onPickerSliderChange, this, true);

                Event.on(this.getElement(ID.WEBSAFE_SWATCH), "click", function(e) {
                       this.setValue(this.get(O.WEBSAFE));
                   }, this, true);

                Event.on(this.getElement(ID.CONTROLS_LABEL), "click", function(e) {
                       this.set(O.SHOW_CONTROLS, !this.get(O.SHOW_CONTROLS));
                       Event.preventDefault(e);
                   }, this, true);

                this._attachRGBHSV(ID.R, O.RED);
                this._attachRGBHSV(ID.G, O.GREEN);
                this._attachRGBHSV(ID.B, O.BLUE);
                this._attachRGBHSV(ID.H, O.HUE);
                this._attachRGBHSV(ID.S, O.SATURATION);
                this._attachRGBHSV(ID.V, O.VALUE);

                Event.on(this.getElement(ID.HEX), "keydown", function(e, me) {
                        me._hexFieldKeypress(e, this, O.HEX);
                    }, this);

                Event.on(this.getElement(this.ID.HEX), "keypress",
                    this._hexOnly, this,true);
                Event.on(this.getElement(this.ID.HEX), "blur", function(e, me) {
                        me._useFieldValue(e, this, O.HEX);
                    }, this);
            },

            /**
             * Wrapper for _updateRGB, but allows setting
             *
             * @method syncUI
             * @param skipAnim {Boolean} Omit Slider animation for this action
             */
            syncUI : function (skipAnim) {
                this.skipAnim = skipAnim;
                this._updateRGB();
                this.skipAnim = false;
            },

            /**
             * Updates the RGB values from the current state of the HSV
             * values.  Executed when the one of the HSV form fields are
             * updated
             * _updateRGBFromHSV
             * @protected
             */
            _updateRGBFromHSV : function() {
                var hsv = [this.get(this.OPT.HUE),
                           this.get(this.OPT.SATURATION)/100,
                           this.get(this.OPT.VALUE)/100],
                    rgb = Color.hsv2rgb(hsv);

                this.logger.log("HSV converted to RGB " + hsv + " : " + rgb);
                this.set(this.OPT.RGB, rgb);

                this._updateSliders();
            },

            /**
             * Parses the hex string to normalize shorthand values, converts
             * the hex value to rgb and updates the rgb attribute (which
             * updates the state for all of the other values)
             * method _updateHex
             * @protected
             */
            _updateHex : function() {

                var hex = this.get(this.OPT.HEX),
                    l   = hex.length,
                    c,i,rgb;

                // support #369 -> #336699 shorthand
                if (l === 3) {
                    c = hex.split("");
                    for (i=0; i<l; i=i+1) {
                        c[i] = c[i] + c[i];
                    }

                    hex = c.join("");
                }

                if (hex.length !== 6) {
                    this.logger.log(this.get(this.TXT.ILLEGAL_HEX), "error");
                    return false;
                }

                rgb = Color.hex2rgb(hex);

                this.logger.log(sub("Hex value set to {hex} ({rgb})", {
                        hex: hex, rgb: rgb
                    }));

                this.setValue(rgb);
            },

            /**
             * Returns the cached element reference.  If the id is not a string, it
             * is assumed that it is an element and this is returned.
             * @param id {string|HTMLElement} the element key, id, or ref
             * @param on {boolean} hide or show.  If true, show
             * @protected
             */
            _hideShowEl : function(id, on) {
                var el = (lang.isString(id) ? this.getElement(id) : id);
                Dom.setStyle(el, "display", (on) ? "" : "none");
            },

            /**
             * Sets up the config attributes and the change listeners for this
             * properties
             * @method initAttributes
             * @param attr An object containing default attribute values
             */
            initAttributes : function(attr) {

                attr = attr || {};
                ColorPicker.superclass.initAttributes.call(this, attr);

                /**
                 * The size of the picker. Trying to change this is not recommended.
                 * @attribute pickersize
                 * @default 180
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.PICKER_SIZE, {
                        value: attr.size || this.DEFAULT.PICKER_SIZE
                    });

                /**
                 * The current hue value 0-360
                 * @attribute hue
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.HUE, {
                        value: attr.hue || 0,
                        validator: lang.isNumber
                    });

                /**
                 * The current saturation value 0-100
                 * @attribute saturation
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.SATURATION, {
                        value: attr.saturation || 0,
                        validator: lang.isNumber
                    });

                /**
                 * The current value/brightness value 0-100
                 * @attribute value
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.VALUE, {
                        value: lang.isNumber(attr.value) ? attr.value : 100,
                        validator: lang.isNumber
                    });

                /**
                 * The current red value 0-255
                 * @attribute red
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.RED, {
                        value: lang.isNumber(attr.red) ? attr.red : 255,
                        validator: lang.isNumber
                    });

                /**
                 * The current green value 0-255
                 * @attribute green
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.GREEN, {
                        value: lang.isNumber(attr.green) ? attr.green : 255,
                        validator: lang.isNumber
                    });

                /**
                 * The current blue value 0-255
                 * @attribute blue
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.BLUE, {
                        value: lang.isNumber(attr.blue) ? attr.blue : 255,
                        validator: lang.isNumber
                    });

                /**
                 * The current hex value #000000-#FFFFFF, without the #
                 * @attribute hex
                 * @type string
                 */
                this.setAttributeConfig(this.OPT.HEX, {
                        value: attr.hex || "FFFFFF",
                        validator: lang.isString
                    });

                /**
                 * The current rgb value.  Updates the state of all of the
                 * other value fields.  Read-only: use setValue to set the
                 * controls rgb value.
                 * @attribute hex
                 * @type [int, int, int]
                 * @readonly
                 */
                this.setAttributeConfig(this.OPT.RGB, {
                        value: attr.rgb || [255,255,255],
                        method: function(rgb) {

                            this.set(this.OPT.RED, rgb[0], true);
                            this.set(this.OPT.GREEN, rgb[1], true);
                            this.set(this.OPT.BLUE, rgb[2], true);

                            var websafe = Color.websafe(rgb),
                                hex = Color.rgb2hex(rgb),
                                hsv = Color.rgb2hsv(rgb);

                            this.set(this.OPT.WEBSAFE, websafe, true);
                            this.set(this.OPT.HEX, hex, true);

                            this.logger.log(sub("RGB value set to {rgb} (hsv: {hsv})", {
                                    "hsv": hsv, "rgb": rgb
                                }));

                            // fix bug #1754338 - when saturation is 0, hue is
                            // silently always set to 0, but input field not updated
                            if (hsv[1]) {
                                this.set(this.OPT.HUE, hsv[0], true);
                            }
                            this.set(this.OPT.SATURATION, Math.round(hsv[1]*100), true);
                            this.set(this.OPT.VALUE, Math.round(hsv[2]*100), true);
                        },
                        readonly: true
                    });

                /**
                 * If the color picker will live inside of a container object,
                 * set, provide a reference to it so the control can use the
                 * container's events.
                 * @attribute container
                 * @type YAHOO.widget.Panel
                 */
                this.setAttributeConfig(this.OPT.CONTAINER, {
                            value: null,
                            method: function(container) {
                                if (container) {
                                    // Position can get out of sync when the
                                    // control is manipulated while display is
                                    // none.  Resetting the slider constraints
                                    // when it is visible gets the state back in
                                    // order.
                                    container.showEvent.subscribe(function() {
                                        // this.pickerSlider.thumb.resetConstraints();
                                        // this.hueSlider.thumb.resetConstraints();
                                        this.pickerSlider.focus();
                                    }, this, true);
                                }
                            }
                        });
                /**
                 * The closest current websafe value
                 * @attribute websafe
                 * @type int
                 */
                this.setAttributeConfig(this.OPT.WEBSAFE, {
                        value: attr.websafe || [255,255,255]
                    });

                var ids = attr.ids || lang.merge({}, this.ID), i;

                if (!attr.ids && _pickercount > 1) {
                    for (i in ids) {
                        if (lang.hasOwnProperty(ids, i)) {
                            ids[i] = ids[i] + _pickercount;
                        }
                    }
                }

                /**
                 * A list of element ids and/or element references used by the
                 * control.  The default is the this.ID list, and can be customized
                 * by passing a list in the contructor
                 * @attribute ids
                 * @type {referenceid: realid}
                 * @writeonce
                 */
                this.setAttributeConfig(this.OPT.IDS, {
                        value: ids,
                        writeonce: true
                    });

                /**
                 * A list of txt strings for internationalization.  Default
                 * is this.TXT
                 * @attribute txt
                 * @type {key: txt}
                 * @writeonce
                 */
                this.setAttributeConfig(this.OPT.TXT, {
                        value: attr.txt || this.TXT,
                        writeonce: true
                    });

                /**
                 * The img src default list
                 * is this.IMAGES
                 * @attribute images
                 * @type {key: image}
                 * @writeonce
                 */
                this.setAttributeConfig(this.OPT.IMAGES, {
                        value: attr.images || this.IMAGE,
                        writeonce: true
                    });
                /**
                 * The element refs used by this control.  Set at initialization
                 * @attribute elements
                 * @type {id: HTMLElement}
                 * @readonly
                 */
                this.setAttributeConfig(this.OPT.ELEMENTS, {
                        value: {},
                        readonly: true
                    });

                /**
                 * Hide/show the entire set of controls
                 * @attribute showcontrols
                 * @type boolean
                 * @default true
                 */
                this.setAttributeConfig(this.OPT.SHOW_CONTROLS, {
                        value: lang.isBoolean(attr.showcontrols) ? attr.showcontrols : true,
                        method: function(on) {

                            var el = Dom.getElementsByClassName("bd", "div",
                                    this.getElement(this.ID.CONTROLS))[0];

                            this._hideShowEl(el, on);

                            this.getElement(this.ID.CONTROLS_LABEL).innerHTML =
                                (on) ? this.get(this.OPT.TXT).HIDE_CONTROLS :
                                       this.get(this.OPT.TXT).SHOW_CONTROLS;

                        }
                    });

                /**
                 * Hide/show the rgb controls
                 * @attribute showrgbcontrols
                 * @type boolean
                 * @default true
                 */
                this.setAttributeConfig(this.OPT.SHOW_RGB_CONTROLS, {
                        value: lang.isBoolean(attr.showrgbcontrols) ? attr.showrgbcontrols : true,
                        method: function(on) {
                            this._hideShowEl(this.ID.RGB_CONTROLS, on);
                        }
                    });

                /**
                 * Hide/show the hsv controls
                 * @attribute showhsvcontrols
                 * @type boolean
                 * @default false
                 */
                this.setAttributeConfig(this.OPT.SHOW_HSV_CONTROLS, {
                        value: lang.isBoolean(attr.showhsvcontrols) ?
                                              attr.showhsvcontrols : false,
                        method: function(on) {
                            //Dom.setStyle(this.getElement(this.ID.HSV_CONTROLS), "visibility", (on) ? "" : "hidden");
                            this._hideShowEl(this.ID.HSV_CONTROLS, on);

                            // can't show both the hsv controls and the rbg hex summary
                            if (on && this.get(this.OPT.SHOW_HEX_SUMMARY)) {
                                this.set(this.OPT.SHOW_HEX_SUMMARY, false);
                            }
                        }
                    });

                /**
                 * Hide/show the hex controls
                 * @attribute showhexcontrols
                 * @type boolean
                 * @default true
                 */
                this.setAttributeConfig(this.OPT.SHOW_HEX_CONTROLS, {
                        value: lang.isBoolean(attr.showhexcontrols) ?
                                              attr.showhexcontrols : false,
                        method: function(on) {
                            this._hideShowEl(this.ID.HEX_CONTROLS, on);
                        }
                    });

                /**
                 * Hide/show the websafe swatch
                 * @attribute showwebsafe
                 * @type boolean
                 * @default true
                 */
                this.setAttributeConfig(this.OPT.SHOW_WEBSAFE, {
                        value: lang.isBoolean(attr.showwebsafe) ? attr.showwebsafe : true,
                        method: function(on) {
                            this._hideShowEl(this.ID.WEBSAFE_SWATCH, on);
                        }
                    });

                /**
                 * Hide/show the hex summary
                 * @attribute showhexsummary
                 * @type boolean
                 * @default true
                 */
                this.setAttributeConfig(this.OPT.SHOW_HEX_SUMMARY, {
                        value: lang.isBoolean(attr.showhexsummary) ? attr.showhexsummary : true,
                        method: function(on) {
                            this._hideShowEl(this.ID.HEX_SUMMARY, on);

                            // can't show both the hsv controls and the rbg hex summary
                            if (on && this.get(this.OPT.SHOW_HSV_CONTROLS)) {
                                this.set(this.OPT.SHOW_HSV_CONTROLS, false);
                            }
                        }
                    });
                this.setAttributeConfig(this.OPT.ANIMATE, {
                        value: lang.isBoolean(attr.animate) ? attr.animate : true,
                        method: function(on) {
                            if (this.pickerSlider) {
                                this.pickerSlider.animate = on;
                                this.hueSlider.animate = on;
                            }
                        }
                    });

                this.on(this.OPT.HUE + "Change", this._updateRGBFromHSV, this, true);
                this.on(this.OPT.SATURATION + "Change", this._updateRGBFromHSV, this, true);
                this.on(this.OPT.VALUE + "Change", this._updateRGBFromHSV, this, true);

                this.on(this.OPT.RED + "Change", this._updateRGB, this, true);
                this.on(this.OPT.GREEN + "Change", this._updateRGB, this, true);
                this.on(this.OPT.BLUE + "Change", this._updateRGB, this, true);

                this.on(this.OPT.HEX + "Change", this._updateHex, this, true);

                this._initElements();
            }
        });

        YAHOO.widget.ColorPicker = ColorPicker;
    })();

Copyright © 2009 Yahoo! Inc. All rights reserved.