Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Wednesday, November 19, 2008

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”.

posted by Matt Snider at 11:38 am  

3 Comments »

  1. that is ten tons of awesome

    Comment by jkm — November 20, 2008 @ 12:06 pm

  2. This is a handy script.

    I noticed that the onClick function clears the node’s innerHTML before it does the getRegion. This causes the width of the input or textarea to be zero if the original node is floated left.

    Thanks for a cool script.

    Comment by Trey Runcie — December 2, 2008 @ 8:01 pm

  3. Trey,

    I never considered if the element was floating or not, but I probably should, since many designs float elements. For now I moved the “innerHTML = ””, although I plan to look into this more later.

    Comment by Matt Snider — December 3, 2008 @ 10:32 am

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress