The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
   Heavily borrowed from the example at:

     http://blog.davclass.com/

   Objectified and extended by:

     Chisel Wright <chisel@herlpacker.co.uk>
*/

/*
    Example usage:

    <script type="text/javascript" src="/path/to/Editable.js"></script>
    <script type="text/javascript">
        // create a new object
        var E  = new YAHOO.widget.EditableElement;
        // override the default callback()
        E.callback = function(){ console.log('my callback called') };
        // initialise the widget
        E.init();
    </script>
*/

(function () {
    YAHOO.widget.EditableElement = function() {
        var Dom             = YAHOO.util.Dom,
            YU              = YAHOO.util;

        this.config = {
            class_name      : 'editable',   // the default classname to make editable
            trigger         : 'click',      // or 'doubleclick'
            input_type      : 'text',       // or 'textarea'

            //min_input_size  : 0,

            save_on_enter   : true,         // does the enter key trigger a Save?
            clear_on_escape : true,         // does the escape key trigger a Cancel?

            linebreak       : false,        // insert a linebreak before the buttons?

            save_button     : true,         // show a Save button?
            cancel_button   : true,         // show a Cancel button?

            textarea_rows   : 6,            // default rows to use if we're
                                            // working with a textarea
            textarea_cols   : 50            // default colums to use if we're
                                            // working with a textarea
        };
        this.clicked  = undefined;
        this.contents = undefined;
        this.input    = undefined;

        // set up the object to monitor relevant DOM elements
        this.init = function() {
            _items = Dom.getElementsByClassName(this.config.class_name);
            if (_items.length > 0) {
                for (i = 0; i < _items.length; i++) {
                    // make sure the item has an id
                    this.elID = YU.Dom.generateId(_items[i]);

                    // add the (double-)click listener
                    YU.Event.addListener(
                        _items[i],
                        this.config.trigger,
                        this.triggered,
                        this,
                        true);

                    /* if we need to handle any key events */
                    if (   this.config.save_on_enter
                        || this.config.clear_on_escape
                    ) {
                        var status = YU.Event.addListener(
                            _items[i],
                            'keyup',
                            this.onKeyUp,
                            this,
                            true
                        );
                    }
                }
            }
        };


        // if I new javascript better I probably wouldn't have to
        // write my own max() function
        this._max = function(x,y) {
            if (x>y) { return x; }
            return y;
        };

        /* when working with a <textarea /> this.clicked can be a node
           _inside_ the node we're monitoring, e.g. if you click on a
           <i>...</i> portion of the text.
           We want to edit the whole block, so we step up to the first node we
           find that has a className matching this.config.class_name
        */
        this.step_up = function(el) {
            if (el) {
                var tmpEl = el;
                while (tmpEl.className != this.config.class_name) {
                    tmpEl = tmpEl.parentNode;
                }
                if (tmpEl) {
                    el = tmpEl;
                }
            }
            return el;
        }

        this.triggered = function(ev) {
            if (! this.check() ) {
                return;
            }

            this.clicked = YU.Event.getTarget(ev);
            this.clicked = this.step_up(this.clicked);

            this.contents = this.clicked.innerHTML;
            this.create_input_field();
        };


        this.onKeyUp = function(p_oEvent) {
            var keyCode = YU.Event.getCharCode(p_oEvent);

            switch (keyCode) {
                case 13: // enter key
                    if (this.config.save_on_enter) {
                        this.check(p_oEvent);
                    }
                    break;

                case 27: // enter key
                    if (this.config.save_on_enter) {
                        // ideally we'd reset the box to its original value here
                        this.reset_input_field();
                    }
                    break;

                default:
                    // do nothing
                    break;
            }
        };


        this.create_input_field = function() {
            this.input = YU.Dom.generateId();

            /*
             * Create a 'input type="text"' element
             */
            if (this.config.input_type == 'text') {
                // create a new element for the input
                new_input  = document.createElement('input');
                min_size   = this._max(this.contents.length, this.config.min_size);
                with (new_input) {
                    setAttribute('type', 'text');
                    setAttribute('id', this.input);
                    value = this.contents;
                    setAttribute('size', min_size);
                    className = 'editable_input';
                }
            }
            /*
             * Create a 'textarea' element
             */
            else if (this.config.input_type == 'textarea') {
                new_input  = document.createElement('textarea');
                with (new_input) {
                    setAttribute('id', this.input);
                    value = this.contents;
                    className = 'editable_input';

                    setAttribute('rows', this.config.textarea_rows);
                    setAttribute('cols', this.config.textarea_cols);
                }
            }

            this.clicked.innerHTML = '';
            this.clicked.appendChild(new_input);

            // insert a line-break before the buttons
            if (this.config.linebreak) {
                newline = document.createElement('br');
                this.clicked.appendChild(newline);
            }

            // show the save button
            if (this.config.save_button) {
                this.create_save_button();
            }

            // show the cancel button
            if (this.config.cancel_button) {
                this.create_cancel_button();
            }

            // select the newly created input field
            new_input.select();
        };


        this.create_save_button = function() {
            // create the save button
            this.save_input  = YU.Dom.generateId();
            new_save_input   = document.createElement('input');
            with (new_save_input) {
                setAttribute('type', 'button');
                setAttribute('id', this.save_input);
                value = 'Save';
                className = 'editable_input';
            }
            // add it to the clicked element
            this.clicked.appendChild(new_save_input);
            // add a listener
            YU.Event.addListener(
                new_save_input,
                'click',
                this.check,
                this,
                true
            );
        };


        this.create_cancel_button = function() {
            // create the cancel button
            this.cancel_input  = YU.Dom.generateId();
            new_cancel_input   = document.createElement('input');
            with (new_cancel_input) {
                setAttribute('type', 'button');
                setAttribute('id', this.cancel_input);
                value = 'Cancel';
                className = 'editable_input';
            }
            // add it to the clicked element
            this.clicked.appendChild(new_cancel_input);
            // add a listener
            YU.Event.addListener(
                new_cancel_input,
                'click',
                this.reset_input_field,
                this,
                true
            );
        };


        this.reset_input_field = function() {
            this.clicked.innerHTML = this.contents;
            this.clicked  = undefined;
            this.contents = undefined;
            this.input    = undefined;
        };


        this.clear_input = function() {
            if (this.input) {
                var input_value = YU.Dom.get(this.input).value;

                if (input_value.length > 0) {
                    this.clean_input();
                    this.contents_new = input_value;
                }
                else {
                    this.contents_new = '[removed]';
                }
                this.clicked.innerHTML = this.contents_new;

                if (this.contents_new != this.contents) {
                    var el = this.step_up(this.clicked);
                    this.callback(el);
                }
            }
            this.clicked  = undefined;
            this.contents = undefined;
            this.input    = undefined;
        };


        this.clean_input = function() {
            checkText   = new String(YU.Dom.get(this.input).value);
            regEx1      = /\"/g;
            checkText       = String(checkText.replace(regEx1, ''));
            YU.Dom.get(this.input).value = checkText;
        };


        this.check = function(ev) {
            if (!ev) {
                if (this.clicked == undefined) {
                    return true;
                }
                return false;
            }
            if (this.clicked) {
                clicked = YU.Event.getTarget(ev);
                this.clear_input();
                return true;
            }
        };


        // a default callback() function; you'll want to assign your own in
        // the object you create
        this.callback = function() {
            alert('default callback() called');
        };
    };
})();