Element Editor

About a week ago, Nik, posed a question about how to record edits in his data table widget. Responding to his inquiry, inspired todays article, which covers the "Element Editor" object (a work in progress). The ultimate goal of the project is to develop a widget that can be used to dynamically update the content of any DOM node, by inserting an editable form field (input, textarea, select, etc.). In version 1, "Element Editor" can replace a DOM node with a textarea or an input, allowing a client to replace the text inside of the DOM node. The widget contains a singleton Custom Event (1 Custom Event shared by all ElementEditor instances), which can be subscribed to, that fires each time a user changes the DOM node content.

Example 1: Element Editor

Core.Widget.ElementEditorEvents = new YAHOO.util.EventProvider();

Core.Widget.ElementEditor = function(elem, conf) {
    var YE = YAHOO.util.Event,
        YD = YAHOO.util.Dom,
        YK = YAHOO.util.KeyListener.KEY,
        CE = Core.Widget.ElementEditorEvents;
    YE.off = YE.removeListener;

    var cfg = conf || {},
        F = function() {},
        node = YD.get(elem),
        text = ,
        that = null;

    // configure
    if (! cfg.tagName) {cfg.tagName = textarea;}
    if (! cfg.type) {cfg.type = text;}
    if (! cfg.cols) {cfg.cols = 5;}
    if (! cfg.rows) {cfg.rows = 5;}
    if (! cfg.name) {cfg.name = ;}

    // event namespace
    var E = {

        onClick: function(e) {
            text = node.innerHTML;
            YE.stopPropagation(e);

            var dim = YD.getRegion(node),
                child = document.createElement(cfg.tagName);

            node.innerHTML = ;

            if (cfg.fitToParent) {
                YD.setStyle(child, height, dim.bottom - dim.top + px);
                YD.setStyle(child, width, dim.right - dim.left + px);
            }

            node.appendChild(child);
            child.value = text;

            if (YAHOO.lang.isFunction(child.focus)) {child.focus();} // focus on the new element
            child.name = cfg.name;

            // special-case for each tag name
            switch (cfg.tagName) {
                case textarea:
                    child.cols = cfg.cols;
                    child.rows = cfg.rows;
                break;

                case input:
                    child.type = cfg.type;
                    if (! cfg.size) {child.size = cfg.size;}
                    if (! cfg.maxLength) {child.setAttribute(maxLength, cfg.maxLength);}
                break;

                default:
            }

            YE.off(node, click, E.onClick);
            YE.on(child, blur, E.onBlur);
            YE.on(node, keydown, E.onKeyDispatcher);
        },

        onBlur: function(e) {
            var s = node.firstChild.value;
            YE.stopPropagation(e);
            
            YE.on(node, click, E.onClick);
            YE.off(node, keydown, E.onKeyDispatcher);
            YE.off(node.firstChild, blur, E.onBlur);

            node.innerHTML = s;
            if (text !== s) {CE.fireEvent(that.CE_CHANGE, {id: cfg.id, element: node, value: s});}
        },

        onKeyDispatcher: function(e) {
            switch (YE.getCharCode(e)) {
                case YK.ENTER:
                    YE.stopEvent(e);
                    E.onBlur(e);
                break;

                case YK.ESCAPE:
                    node.firstChild.value = text;
                    E.onBlur(e);
                break;

                default:
            }
        }
    };

    YE.on(node, click, E.onClick);

    // public namespace
    F.prototype = {

        CE_CHANGE: elementEditorSave,

	subscribe: function(p_type, p_fn, p_obj, p_override) {
		CE.subscribe(p_type, p_fn, p_obj, p_override);
	}
    };

    that = new F();

    // lazily inialize custom events
	if (! CE.hasEvent(that.CE_CHANGE)) {
		CE.createEvent(that.CE_CHANGE);
	}

    return that;
};

First, the singleton event provider is initialized. In your own projects, if you already use a global event provider, you can just hook into it. The ElementEditor object is instantiated by passing a reference to the DOM node and an optional configuration parameter. When initializing, the object first creates local pointers to YUI, initializes some internal variables, then defines default configuration. The configuration is still basic, allowing the engineer to specify several attributes of the editable field created by the widget, but will become more complex and less restrictive in future versions. Next the event callbacks are defined, the click event is subscribed too, and a public subscribe method is defined to expose the Custom Event. Lastly, if the Custom Event singleton is not already defined, we go ahead and initialize it.

All the magic begins, when the client clicks on the node that was passed in to initialize the ElementEditor. After clicking: the content of the node is stored into a text variable, the editable field replaces all children of the node, and the click event is unsubscribed whilst blur and keydown events are subscribed to. If the fitToParent parent configuration is set, then the new edit element will be sized to fill the parent node. Also, if the native JavaScript focus function is available on the newly created field (textareas and inputs), then we focus on the field as well.

The keydown callback listens for the escape key and the enter key, dispatching the right actions accordingly. Escape will restore the previous content and ignore any changes that the user has made. Whilst the enter key will forward to the blur event handler. The blur callback unsubscribes the keydown and blur events, before resubscribing the click event. It also, replaces content of the node with the client generated value in the editable field. Lastly, it will fire the save Custom Event (CE_CHANGE) if the client has made any changes. Any callback methods subscribing to CE_CHANGE will be passed an object as the first (and only) parameter, containing the keys: id, element, value, where id is a unique key defined by the object configuration, element is the node, and value is the client generated content.

This widget can be used with a table cell in a data grid, but the goal is to allow the editing of any DOM node. Here is a couple of examples of how to use "Element Editor".