Ajax.InPlaceEditor

Controls > Ajax.InPlaceEditor

Introduction

The in-place “text edit” testing allows for Flickr-style AJAX-backed “on-the-fly” textfields.
See the documentation on Ajax.InPlaceEditor and Ajax.InPlaceCollectionEditor

Syntax

new Ajax.InPlaceEditor( element, url, {options});

The constructor takes three parameters. The first is the element that should support in-place editing. The second is the url to submit the changed value to. The server should respond with the updated value (the server might have post-processed it or validation might have prevented it from changing). The third is a hash of options.
The server side component gets the new value as the parameter ‘value’ (POST method), and should send the new value as the body of the response.

Options

Name since default Description
okControl V?? “button” What type of ok button to use in edit mode, or none at all (button, link, false)
cancelControl V?? “link” What type of cancel button to use in edit mode, or none at all (button, link, false)
okText V1.5 “ok” The text of the submit button that submits the changed value to the server
cancelText V1.5 “cancel” The text of the link that cancels editing
savingText V1.5 “Saving…” The text shown while the text is sent to the server
clickToEditText V1.6 “Click to edit” The text shown during mouseover the editable text
formId V1.5 id of the element to edit plus ‘InPlaceForm’ The id given to the element
externalControl V1.5 null ID of an element that acts as an external control used to enter edit mode. The external control will be hidden when entering edit mode and shown again when leaving edit mode.
externalControlOnly V1.5 false Whether or not to disable onclick editing so that only an external control can activate editable mode
rows V1.5 1 The row height of the input field (anything greater than 1 uses a multiline textarea for input)
onComplete V1.6 “function(transport, element) {new Effect.Highlight(element, {startcolor: this.options.highlightColor});}” Code run if update successful with server. Also if user cancels the form. See bug.
onFailure V1.6 “function(transport) {alert(“Error communicating with the server: ” + transport.responseText.stripTags());}” Code run if update failed with server
cols V1.5 none The number of columns the text area should span (works for both single line or multi line)
size V1.5 none Synonym for ‘cols’ when using single-line (rows=1) input
highlightColor ? Ajax.InPlaceEditor.defaultHighlightColor The highlight color. Must be six hex digits, not 3.
highlightEndColor ? ”#FFFFFF” The color which the highlight fades to. Must be six hex digits, not 3.
savingClassName V1.5 “inplaceeditor-saving” CSS class added to the element while displaying “Saving…” (removed when server responds)
formClassName V1.5 “inplaceeditor-form” CSS class used for the in place edit form
hoverClassName ? ? ?
loadTextURL V1.5 null Will cause the text to be loaded from the server (useful if your text is actually textile and formatted on the server)
loadingText V1.5 “Loading…” If the loadText URL option is specified then this text is displayed while the text is being loaded from the server
callback V1.5 function(form) {Form.serialize(form)} A function that will get executed just before the request is sent to the server, should return the parameters to be sent in the URL. Will get two parameters, the entire form and the value of the text control.
submitOnBlur V1.6 “false” This option if true will submit the in_place_edit form when the input tag loses focus.
ajaxOptions V1.5 {} Options specified to all AJAX calls (loading and saving text), these options are passed through to the prototype AJAX classes.

The server side component gets the new value as the parameter ‘value’ (POST method), and should send the new value as the body of the response.

Character encoding

The form data is sent encoded in UTF-8 regardless of the page encoding. This is as of the prototype function Form.serialize. More info on here.

If this doesn’t work, you can use iconv as outlined here. Link Dead!

Removing the behavior

To disable the InPlaceEditor behavior later on, store it in a variable like:


 var editor = new Ajax.InPlaceEditor('product_1',...);
 (... do stuff ..)
 editor.dispose();

This way, you can enable and disable " In Place Editing ":http://madrobby.github.com/scriptaculous/in-place-editing at will.

You can also arbitrarily force it into edit mode like so:


editor.enterEditMode('click');

Demo1 (Single Line Edit Mode)

note: the demo doesn’t actually save its changes, as demoajaxreturn.html is not a valid url.


<p id="editme">Click me, click me!</p>
<script type="text/javascript">
 new Ajax.InPlaceEditor('editme', '/demoajaxreturn.html');
</script>

Click me, click me!

Demo2 (Multi Line Edit Mode)


<p id="editme2">Click me to edit this nice long text.</p>
<script type="text/javascript">
 new Ajax.InPlaceEditor('editme2', '/demoajaxreturn.html', {rows:15,cols:40});
</script>

Click me to edit this nice long text.

How to specify a different name for the parameter sent to the server

Add a callback function which is supposed to return the parameters that is sent to the server. Like this:


new Ajax.InPlaceEditor('id', 'url', {
callback: function(form, value) { return 'myparam=' + encodeURIComponent(value) }
})

The encodeURIComponent() makes sure values containing special characters like “&” or “=” don’t cause problems. Use encodeURIComponent() instead of escape() from previous examples to get UTF-8 encoded data (and not “malformed URI” errors in Firefox). This function can also be used to pass additional parameters, such as what item or field to edit:


new Ajax.InPlaceEditor('id', 'url', { 
callback: function(form, value) { return 'item=123&field=description&myparam='+encodeURIComponent(value) }
})

Note: this parameters name may change (to “parameters” or “with”) before the final release of 1.5.

How to execute custom code using the InPlaceEditor’s callbacks.


new Ajax.InPlaceEditor(id, url, {
callback: function(form, value) { return 'item=123&field=description&myparam='+encodeURIComponent(value) },
onEnterEditMode: function(form, value) { // custom code goes here... },
onLeaveEditMode: function(form, value) { // custom code goes here... }
});

You can find all the possible callbacks by looking for lines in the source like this:


this.triggerCallback('onEnterEditMode');

(Should these callback options be documented in the Options list above?)

How to add a spinning in progress indicator

Get an animated .gif that shows progress (the spinning image used on the Macintosh for example). Add a CSS class for ‘inplaceeditor-saving’ with the spinning image as background. The class will be added to the element during communication with the server and removed afterwards.


<style type="text/css" media="screen">
  .inplaceeditor-saving { background: url(/images/wait.gif) bottom right no-repeat; }
</style>

<p id="editme3">Click me, click me!</p>
<script type="text/javascript">
 new Ajax.InPlaceEditor('editme3', '/demoajaxreturn.html');
</script>

How to customize the look and feel of the form

Use the id or class (‘inplaceeditor-form’) of the form in a CSS selector to apply the desired style. The text field is always named ‘value’, the ok button is an input with type ‘submit’ and the cancel link is a normal anchor (‘a’) element.


form.inplaceeditor-form { /* The form */
}

form.inplaceeditor-form input[type="text"] { /* Input box */
}

form.inplaceeditor-form textarea { /* Textarea, if multiple rows */
}

form.inplaceeditor-form input[type="submit"] { /* The submit button */
  margin-left:1em;
}

form.inplaceeditor-form a { /* The cancel link */
  margin-left:1em;
}

How to edit server side formatted text (formatted with eg. textile)

Format the text on the server when the initial page is rendered. Add a new server side action that returns the unformatted text in the reponse and specify the load Text URL? option as a URL to this action. The control will load the text from the server instead of using the text on the page. The action that receives the updated text should save it and return the formatted text as an HTML snippet in the reponse.

Example using Ruby on Rails

#controller
def my_action
#get the text to display initially
@fancy_text = “Some fancy bold and emphasized text.”
end

def update_text
#save your text here, and return the saved value
@fancy_text = params[:value]
render :layout => false, :inline => “<%= textilize( @fancy_text ) %>”
end

def return_unformatted_text
#get your text and return it without the formatting
@fancy_text = “Some fancy edited, bold and emphasized text.”
render :layout => false, :inline => “<%= @fancy_text %>”
end

#view ( of my_action.rhtml )

<%= textilize( @fancy_text ) %>

In-Place edits with select lists

In my (java) application, I have a need for in-place editing in the form of select lists. Given a server-side ajax helper that provides the necessary options for the current user, I was able to write the following:


function setupCategoryEditor(el, url) {
    var editor=new Ajax.InPlaceEditor(el, url);
    Object.extend(editor, {
        createEditField: function() {
            var text=this.getText();

            var field=document.createElement("select");
            field.name="value";

            this.editField=field;
            this.form.appendChild(this.editField);

            new Ajax.Request('/url/to/option/list', {
                onSuccess: function(req) {
                    // Get the text from an XML tag.
                    var getData=function(el, which) {
                        stuff=el.getElementsByTagName(which);
                        return stuff[0].firstChild.nodeValue;
                    };
                    var cats=req.responseXML.getElementsByTagName("cat");
                    $A(cats).each( function(cat, idx) {
                        var op=document.createElement("option");
                        op.value=getData(cat, "value");
                        op.text=getData(cat, "key");
                        if(window.ActiveXObject) {
                            field.options.add(op);
                        } else {
                            field.appendChild(op);
                        }

                        // Select the current item
                        if(op.text == text) {
                            field.selectedIndex=idx;
                        }
                    });
                }
                });
        }
    });
}

Obviously, one could write something smaller with JSON or innerHTML if you’re into that kind of thing.

There is another implementation of " In Place Editor “:http://madrobby.github.com/scriptaculous/in-place-editor for select lists called Ajax. ”color:#aaa"> In Place Select? available here.

You can change getText like so:

getText: function() {
  return this.element.childNodes[0] ? this.element.childNodes[0].nodeValue : '';
},

to avoid e.g. “Me & Myself” being represented in the input box as “Me & amp; Myself” (use nodeValue because innerText does not work with Safari when the element is hidden).

- – -
I just added inplace editor inside a sortable list array. I can drag and after, the editor appears, but ideally I would not want that to happen and instead activate the editor specifically, not as a result of dragging my list items. What is suggested here?
I tried using a drag handle, which seemed like an obvious solution, except the editor then shows the span class code as well, which is a caveat I suppose.
Unless I can use a drag handle without the method in example, then that won’t be a good solution.

How else can I drag or how else can I activate the editor besides writing redundant for each instance?
-ms

I found a workaround to use draggable and Ajax.InPlaceEditor at same time. I faced also this problem mentioned above (the ‘mousup’ action at the end of the draggable activates the editor). The solution that I found is to modify control.js to activate InPlaceEditor only on dbclick instead of click. The drawback is that you should deal with navigator compatibilty… So It consists in replacing:


 Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }

by

 Listeners: {
    dblclick: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }

-Tonio
- – -

I just spent a good deal of time figuring that out myself so I’ll share my result:


<style>
div.handle {
    display: none;
    float: left;
    width: 25px;
    height: 11px;
    margin-top: 2px;
    padding-right: 3px;
    background: url('/images/drag.gif');
    background-repeat: no-repeat;
    clear: both;
}
</style>
<ul id="field">
    <li id="field_1">

        <div class="handle"></div><div class="delete"></div>
        <span class="data" id="field_name_1">Brand</span></li>
    <li id="field_3">
        <div class="handle"></div><div class="delete"></div>

        <span class="data" id="field_name_3">SKU Number</span></li>
    <li id="field_5">
        <div class="handle"></div><div class="delete"></div>
        <span class="data" id="field_name_5">Color</span></li>

</ul>
<a id="field_sort" href="javascript:;" onclick="makeSortable('field');">Reorder</a>
<a id="field_donesort" href="javascript:;" onclick="doneSorting('field');" style="display:none;">Done Reordering</a>
<script>
function makeSortable(myid,mytag){
    Sortable.create(myid,{ tag: mytag || 'li', handle:'handle'});
    setDisplayByClass('handle','inline',$(myid));
    $(myid+'_sort').style.display = 'none';
    $(myid+'_donesort').style.display = '';
    $(myid).setAttribute('reordering','yes');
    return false;
}
function doneSorting(myid){
    new Ajax.Updater('reply',page,{parameters:'a=reorder&table='+myid+'&'+Sortable.serialize(myid)});
    Sortable.destroy(myid);
    setDisplayByClass('handle','none',$(myid));
    $(myid+'_sort').style.display = '';
    $(myid+'_donesort').style.display = 'none';
    $(myid).setAttribute('reordering','no');
    return false;
}
var list = getElementsByClass('data',$('fields'));
var ipeOptionsStd = {
        callback:    function(form,value){
                        var id=form.id;
                        id = id.substring(0,id.indexOf('-inplaceeditor'));
                        var t = id.substring(0,id.indexOf('_'));
                        var c = id.substring(0,id.lastIndexOf('_'));
                        var i = id.substring(id.lastIndexOf('_')+1,id.length);
                        return 'a=update&table='+t+'&column='+c+'&id='+i+'&value='+encodeURIComponent(value);
                    }
        };
for(var i=0;i<list.length;i++){
    new Ajax.InPlaceEditor(list[i], page, ipeOptionsStd );
}

</script>

Hope that helps, that is a much simplified version. I’ve also done this with tables where each cell is an " In Place Editor ":http://madrobby.github.com/scriptaculous/in-place-editor and the rows are Sortable.

- Colin

Using HTML within the In Place Editor

If you are using Markdown or Textile, you can include HTML within your text. And if you follow the instructions above, you will be able to load this text in from your database with loadTextURL. But the stripTags function is invoked by the In Place Editor, and that strips out all the code before it can be displayed in the editor field.

To get around this, add the following two extensions anywhere after the rest of the library has loaded. I use the extensions.js method noted elsewhere on this site.


Object.extend(Ajax.InPlaceEditor.prototype, {
    onLoadedExternalText: function(transport) {
        Element.removeClassName(this.form, this.options.loadingClassName);
        this.editField.disabled = false;
        this.editField.value = transport.responseText;
        Field.scrollFreeActivate(this.editField);
    }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
    getText: function() {
        return this.element.childNodes[0] ? this.element.childNodes[0].nodeValue : '';
    }
});

If you do this, make sure that your loadTextURL does not return entity-encoded text (using htmlentities() for example) or you will end up with double-encoded text.

In-Place edits only by externalControl

i searched for a way to allow editing of certain fields only by clicking the external control field. currently its implemented to allow a user to click either on the element itself or on the external control element.

to only allow editing by clicking on the external control, simply change a few lines:


Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);

if (this.options.externalControl) {
  Event.observe(this.options.externalControl, 'click', this.onclickListener);
  Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
  Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}

change to:

if (this.options.externalControl) {
  Event.observe(this.options.externalControl, 'click', this.onclickListener);
  Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
  Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
} else {
  Event.observe(this.element, 'click', this.onclickListener);
  Event.observe(this.element, 'mouseover', this.mouseoverListener);
  Event.observe(this.element, 'mouseout', this.mouseoutListener);
}

maybe someone can add a flag like ‘only External Control ? to add this to the trunk ..

Small extension to editor to add a text in case field is empty.


/*
 * InPlaceEditor extension that adds a 'click to edit' text when the field is 
 * empty.
 */
Ajax.InPlaceEditor.prototype.__initialize = Ajax.InPlaceEditor.prototype.initialize;
Ajax.InPlaceEditor.prototype.__getText = Ajax.InPlaceEditor.prototype.getText;
Ajax.InPlaceEditor.prototype.__onComplete = Ajax.InPlaceEditor.prototype.onComplete;
Ajax.InPlaceEditor.prototype = Object.extend(Ajax.InPlaceEditor.prototype, {

    initialize: function(element, url, options){
        this.__initialize(element,url,options)
        this.setOptions(options);
        this._checkEmpty();
    },

    setOptions: function(options){
        this.options = Object.extend(Object.extend(this.options,{
            emptyText: 'click to edit...',
            emptyClassName: 'inplaceeditor-empty'
        }),options||{});
    },

    _checkEmpty: function(){
        if( this.element.innerHTML.length == 0 ){
            this.element.appendChild(
                Builder.node('span',{className:this.options.emptyClassName},this.options.emptyText));
        }
    },

    getText: function(){
        document.getElementsByClassName(this.options.emptyClassName,this.element).each(function(child){
            this.element.removeChild(child);
        }.bind(this));
        return this.__getText();
    },

    onComplete: function(transport){
        this._checkEmpty();
        this.__onComplete(transport);
    }
});

To use, copy this into a extensions.js and load it in your app after the other scriptaculous stuff.

Here is the style I use with this:



/* ----  InPlaceEditor style --------------------------------------------- */

.inplaceeditor-empty {
    font-style: italic;
    color: #999;
}

- Pedro

This doesn’t seem to work for me in Prototype 1.6, so I wrote an updated version. (Blog Post).



Ajax.InPlaceEditorWithEmptyText = Class.create(Ajax.InPlaceEditor, {

  initialize : function($super, element, url, options) {

    if (!options.emptyText)        options.emptyText      = “click to edit…“;
    if (!options.emptyClassName)   options.emptyClassName = “inplaceeditor-empty“;

    $super(element, url, options);

    this.checkEmpty();
  },

  checkEmpty : function() {

    if (this.element.innerHTML.length == 0 && this.options.emptyText) {

      this.element.appendChild(
          new Element(“span“, { className : this.options.emptyClassName }).update(this.options.emptyText)
        );
    }

  },

  getText : function($super) {

    if (empty_span = this.element.select(“.“ + this.options.emptyClassName).first()) {
      empty_span.remove();
    }

    return $super();

  },

  onComplete : function($super, transport) {

    this.checkEmpty();
    return $super(transport);

  }

});

- Nik

Create your own Callback

I wanted to have a custom callback that would fire after the InPlaceEditor form was added to the DOM. onEnterEditMode fires as soon as you click, but the form doesn’t exist yet.

This works by copying an existing method enterEditMode(), which builds the form and writes it to the page, into a new method name so that I can edit the real one. In mine, the original is called via __enterEditMode() and then my custom code is run:


Ajax.InPlaceEditor.prototype.__enterEditMode = Ajax.InPlaceEditor.prototype.enterEditMode;
Object.extend(Ajax.InPlaceEditor.prototype, {
  enterEditMode:function(e) {
    this.__enterEditMode(e);
    this.triggerCallback('onFormReady',this._form);
  }
});

Now when I create my InPlaceEditor I just add a the onFormReady callback as an option:


new Ajax.InPlaceEditor('location', '/change_location',
  { 
    onFormReady: function(obj,form) {
      form.getInputs().first().value = '';
    }
  });

In my case I wanted to clear out the value in the text box so that the user had to type something from scratch. If they cancel it will switch back to the original text.

- Rob


Validation.

Information for implementing validation with ajax." In Place Editor ":http://madrobby.github.com/scriptaculous/in-place-editor can be found here. Link Dead!

suggest

If “cancel control” type was ‘button’ then… javascript error occurs.


  new Ajax.InPlaceEditor(node, 'items.aspx', {
	cancelControl: 'button'
  });

So, I suggest the method modification of ‘handleFormCancellation’.


  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
    return false;
  },