Creating a full featured editable component that handles a number of scenarios is quite complex. Some time ago I started in on this process with a wwEditable plug-in which I’ve used in a number of applications. It works, but it’s fairly large and even so has a few rough edges for full generic use.
However, if you’re just after a quick and dirty mechanism for editing some text for the purpose of updating it there’s actually an easier way using the contentEditable attribute on DOM elements. The nice thing about contentEditable is that it maintains original markup and is in effect live editing of text in the currently formatted text. So if unlike wwEditable which used a textbox to copy content and had to figure out what CSS attributes to copy to make the text look right and then handle markup conversion issues, the text you type into a contentEditable block is live HTML.
Some time ago I created this behavior as a way to quickly edit my comments posted here on the WebLog. When I reply, more on a few occasions I’ve been rather sloppy and need to fix a couple of things. Unlike you – dear readers – I can remove posts, but even so that process required copying the old comment, then adding a new comment and hoping the text works. Occasionally there’s also been a request by users to make a small change to a comment – and before I added the ability to edit there was no easy way for me to do this.
With the editable ability I can just tweak the text briefly and in-place. Here’s the regular comment display you see:
and here is what I see when I’m editing a comment:
Notice that the text here becomes just editable inside of the formatting. You can see it where I added the new line code and it inherits the current <pre> formatting (but not the auto C# formatting – that’s another story :-}). There’s also a Save button that pops underneath the content.
Let’s see how this works. The process is pretty simple. I’m using jQuery to select the editable area, make it editable using the contentEditable attribute on the DOM element and then dynamically add the button beneath the text. Then when the save button is clicked an AJAX service callback is made to send the HTML of the edited text back to the server. Finally the button is removed and the display formatting reset. To the user the experience is very smooth and nearly instant because the text is updated in place and the Web Service call to update the text happens in the background.
My first shot at this was to just create a simple non-generic function which is actually fairly short. To give you some context, here’s what the HTML layout of a comment looks like here on the Weblog:
<div class="comment commentauthor" id="cmt_635331">
<a href="#635331" name="635331">#</a>
<img hspace="5" align="right" style="opacity: 0.75;" src="http://www.gravatar.com/avatar.php?gravatar_id=xxx"/>
<b>re: Using Enums in List Controls</b>
<img style="border-width: 0px;" src="../images/EditWhite.gif" class="hoverbutton commentedit"/>
<br/>
<small> by
<a target="_WebLog" href="http://www.west-wind.com/">Rick Strahl</a>
<span class="commenttime">February 22, 2009 @ 2:21 pm</span>
<div class="commentbody">
@Mark - you caught me :-}… rest of content here
</div>
<a onclick="DeleteComment( 635331 );return false;" href="javascript:{}">Remove Comment</a>
</small>
</div>
You can see there’s an image icon with a .commentedit style attached to it which triggers the editing operation. It’s hooked up in $().ready() with a click handler assignment which in turn calls the actual editing routine (note the button behavior and activation click isn’t included in the behavior I’ll describe – maybe in the future):
$().ready(function() {
$(".commentedit").click(commentEdit);
});
function commentEdit(evt) {
var jComment = $(this).parents(".comment").find(".commentbody");
if (jComment.length < 1)
return;
jComment.get(0).contentEditable = true;
jComment
.css( {background:"azure",padding: 10} );
// create button and hookup click handler
var jButton = $("<input type='button' value='Save' />")
.click(function() {
// find the id on the .comment item and strip off cmt_ prefix
// id="cmt_111"
var id = jComment.parents(".comment").get(0).id.replace("cmt_", "");
// Call Web Service with numeric id and updated html
Proxy.UpdateCommentText(+id, jComment.html());
jComment.get(0).contentEditable = false;
// remove button and reset content display
jComment
.css({ background: "transparent", padding: "20px 0 0" });
jButton.remove();
});
jComment.after(jButton);
jButton.after("<br />").css("margin", 5);
}
Not a lot of code here for inline editing. The code starts by checking if the .comment item was found. Next the actual comment item is made editable with the simple jComment.get(0).contentEditable = true. contentEditable is a DOM property (not a jQuery property) and so .get(0) is used to get the first element and assign the property. Voila that’s really all you need to make something editable. BTW, this works on all modern browsers in recent versions which actually surprised me when I first looked at this: IE 6+, FireFox, Safari, Chrome and Opera all work.
The rest of the code deals with adding the save button and it’s action when clicked. When clicked the code finds the parent Comment element and extracts the comment ID out of it. I generate IDs on the server when the comments are created in a ListView with a cmt_ prefix plus the actual comment id so I have a way to get a unique comment id to link to on the page as well as for this update editing.
In this case the save action calls a Web Service (Proxy.UpdateCommentText) which takes the HTML entered and posts it to the server. Then the edit field is returned to its original display state (non-editable) and the save button is removed.
The server is an AJAX service saves the HTML and thus the server is updated. I’m using the AjaxMethodCallback control from the West Wind Web Toolkit here:
<ww:AjaxMethodCallback runat="server" ID="Proxy" ServerUrl="~/WebLogCallbacks.ashx" ></ww:AjaxMethodCallback>
and server method that handles the callback looks like this:
[CallbackMethod]
public bool UpdateCommentText(int id, string html)
{
if (!this.IsAdmin())
throw new AccessViolationException("Access denied - must be logged in");
busComment comment = new busComment();
if (!comment.Load(id))
throw new ArgumentException("Invalid comment Id");
// Content area includes a couple of leading line breaks that we
// don't want in our HTML markup
html = StringUtils.TrimStart(html.Trim(),"<br>",true).Trim();
comment.Entity.Body = html;
return comment.Save();
}
private bool IsAdmin()
{
if (HttpContext.Current.User.Identity != null && HttpContext.Current.User.Identity.IsAuthenticated)
return true;
return false;
}
This is using the West Wind Web Toolkit, but the code would be very similar if you ASP.NET AJAX and an ASMX/WCF Service.
Note that I simply accept the HTML and allow it to be saved. In my scenario here this is acceptable since this is an administrative function. Only administrative users have access, everybody else is bounced. If this was an open connection I’d have to be very, very careful and worry about script injection. While the input typed will be safe since the HTML will be encoded by the Web Browser itself (it creates properly encoded HTML text), there’s always direct HTTP access by a malicious user or script kiddie. Make sure when you update strings as raw HTML over the wire you think about the possible security implications for script injection. Of course you don’t have to send HTML – you can return the .text() value and treat the content entered as text rather than HTML which for many applications will be perfectly valid (think FaceBook’s text editing for example – it’s only text). You still have to worry about script injection though either way you look at it.
Pretty cool though how easy the base process is, right? contentEditable sure is a lot easier to work in a plug-in than having to add a textbox and try to match the overall text formatting. Here all you can do is apply contentEditable = true and you get live editing in the current format. Sweet!
Take Two: Creating a more generic contentEditable Plug-in
The code for doing this sort of inplace editing is not terribly complex, but it does take a little bit of tweaking to remember the right properties to access and add a button, so almost as soon as I had this working I figured this needs to be a jQuery plug-in. As simple as the code above is, creating a plug-in ends up being a little more involved as you start looking at things from a more generic usage perspective.
The generic version is a bit more code, but it’s also a bit more flexible:
$.fn.contentEditable = function(opt) {
if (this.length < 1)
return;
var oldPadding = "0px";
var def = { editClass: null,
saveText: "Save",
saveHandler: null
};
$.extend(def, opt);
return this.each(function() {
var jContent = $(this);
if (this.contentEditable == "true")
return this; // already editing
var jButton = $("<input type='button' value='" + def.saveText + "' class='editablebutton' style='display: block;'/>");
var cleanupEditor = function() {
if (def.editClass)
jContent.removeClass(def.editClass);
else
jContent.css({ background: "transparent", padding: oldPadding });
jContent.get(0).contentEditable = false;
jButton.remove();
};
jButton.click(function(e) {
if (def.saveHandler.call(jContent.get(0), e))
cleanupEditor();
});
jContent.keypress(function(e) {
if (e.keyCode == 27)
cleanupEditor();
else if(e.keyCode == 9)
});
jContent
.after(jButton)
.css("margin", 2);
this.contentEditable = true;
if (def.editClass)
jContent.addClass(def.editClass);
else {
oldPadding = jContent.css("padding");
jContent.css({ background: "lavender", padding: 10 });
}
return this;
});
}
jQuery plug-ins are very easy to create by extending the jQuery.fn object with a custom function that receives the a jQuery object of all the selected elements in the current chain. Typically a plug-in needs to run through each of the elements (typically with a .foreach() ) and then apply the appropriate functionality inside of the foreach() operation. If the plug-in is chainable it should return the jQuery object (this or in this case the result from this.foreach() which is the jQuery object).
So this version adds a few options since it’s generic:
editClass
The CSS class applied to the edited element. If not specified an azure background and 10px padding is applied.
saveText
The text for the save button. The button also has a CSS class of editablebutton applied to it in case you need to override the button display format.
saveHandler
The handler that is called when the save button is clicked. The handler is passed the click handler’s jQuery Event object and the call is made in the context of the edited content element (ie this=content not the button). This is a little unusual – but it makes sense in this circumstance as you want to have access to the content element not the button (which you can still access with $(e.target) if necessary).
In addition you can also press ESC to abort editing without calling the save handler.
The rest of the code should be fairly familiar from the non-generic version. The base operation is similar but there are a few extra checks like whether the item is already being edited and making sure that the display is reset when done editing.
Using this plug-in the application JavaScript code gets a little simpler:
function commentEdit(evt) {
var jComment = $(this).parents(".comment").find(".commentbody");
jComment.contentEditable(
{ editClass: "contenteditable",
saveHandler: function(e) {
// grab id from parent .comment element and strip cmt_ prefix
var id = jComment.parents(".comment").get(0).id.replace("cmt_", "");
// call service to update comment with numeric id and updated html
Proxy.UpdateCommentText(+id, jComment.html());
// return true to close editor (false leaves open)
return true;
}
});
}
I’ve created a sample page so you can check this out:
http://www.west-wind.com/WestwindWebToolkit/samples/Ajax/plugins.aspx
it’s kinda silly, but you get the idea how it works.
Storage Formatting for Text
This is a fairly simple plug-in and it’s not meant as an end all editor and certainly not as a rich text editor. It simply allows you to edit inline content smoothly. Personally I prefer this sort of inline editing to slapping a text box into place as I did with the wwEditable plug-in mentioned earlier.
But because the content you’re entering is HTML you need to think about how you want to manage the data entered: Do you want to treat it as HTML as I did in my blog comment editing or do you treat it as raw text and lose all formatting? It’s really up to you or rather up to the way you store text in your application.
There are really two major approaches that can be taken by applications to store text: You store the raw text the user entered and format the text on the fly as the page is rendered. Here on the WebLog for example, the text you enter in comments is post processed a bit to handle URL expansion and code formatting. I can store the data in the original entered formated or I can choose to turn the text into HTML and store the HTML in the database instead.
I actually chose the original path because originally comments where one-way only. They were never to be edited. Obviously that’s changed now though and I’m wondering if that was the right choice now. The reason to store HTML is that you don’t want to have to re-format the data all the time. I have a few posts that a couple hundred comments and if those all had to be re-formatted it’d be somewhat resource intensive. (it also seems a good idea to start thinking about limiting comments :-})
So in my case the raw HTML editing actually works well as long as it’s an admin only operation via AJAX to avoid malicious data input via scripting.
Note that my example returns the .html() to the server:
Proxy.UpdateCommentText(+id, jComment.html());
but you can just as easily just return the text (.text() instead of .html()). In fact, in many text scenarios that is all that’s really needed.
Since I created this component I’ve been using it in a number of admin interfaces and have added it to the ww.jquery.js client library so it’s always there in my base lib. You can grab the latest code for the plug-in from repository (there are no dependencies for this plug-in), so if you just want this plugin, you can copy it. Documentation can be found here and if you want to get the sample (or any other of the samples) it’s part of the Web Toolkit download and in the full Subversion repository.
Other Posts you might also like