Edit text

See sections 8D and 8E of the book for an explanation of the methods and properties used in this script.

The TEXTAREA behaves strangely in Explorer 5 on Mac: it falls over the other elements, instead of stretching up the page, and initially not all text is shown. Move your mouse up to see all the text. It doesn't register hard returns, either.

Explorer and Opera refuse to accept block level elements inside the test paragraphs.

On this page I give a script for updating a page which can be very useful in a content management system. Click on any paragraph and you can edit the text. When you're done, press the button and the new text shows up normally.

Example

This entire page is the example. Click on any paragraph, edit it, then press 'Ready'. Your changes will show up in the page.

Problems

First of all some problems I encountered. I want to use a textarea as the edit field. First I didn't get the content into the TEXTAREA. An alert reader discovered that Mozilla needs the value to be set after inserting the textarea into the document.

Besides, the content doesn't wrap nicely in Mozilla, it retains the hard returns from the source code. I experimented with several values of the WRAP attribute, but doing nothing gave the best result in the end.

The worst problem is sending the altered text back to the server, something any content management system will want to do.
Readers have sent me various ingenious suggestions to do this. However, since it cannot be done through JavaScript (yet) I don’t treat the solution to this problem on my site. So please don't mail me that you found this or that way to do it: it's certainly possible, but I'm only interested in a pure JavaScript solution without any server side scripts.

The script

var editing  = false;

if (document.getElementById && document.createElement) {
	var butt = document.createElement('BUTTON');
	var buttext = document.createTextNode('Ready!');
	butt.appendChild(buttext);
	butt.onclick = saveEdit;
}

function catchIt(e) {
	if (editing) return;
	if (!document.getElementById || !document.createElement) return;
	if (!e) var obj = window.event.srcElement;
	else var obj = e.target;
	while (obj.nodeType != 1) {
		obj = obj.parentNode;
	}
	if (obj.tagName == 'TEXTAREA' || obj.tagName == 'A') return;
	while (obj.nodeName != 'P' && obj.nodeName != 'HTML') {
		obj = obj.parentNode;
	}
	if (obj.nodeName == 'HTML') return;
	var x = obj.innerHTML;
	var y = document.createElement('TEXTAREA');
	var z = obj.parentNode;
	z.insertBefore(y,obj);
	z.insertBefore(butt,obj);
	z.removeChild(obj);
	y.value = x;
	y.focus();
	editing = true;
}

function saveEdit() {
	var area = document.getElementsByTagName('TEXTAREA')[0];
	var y = document.createElement('P');
	var z = area.parentNode;
	y.innerHTML = area.value;
	z.insertBefore(y,area);
	z.removeChild(area);
	z.removeChild(document.getElementsByTagName('button')[0]);
	editing = false;
}

document.onclick = catchIt;

Explanation

We set a flag editing to false. This shows whether the user is currently editing a paragraph. Initially he isn't, of course.

var editing  = false;

Create the button

Then we create the 'Ready' button that we'll need several times. For this we need the most advanced scripting, so first some object detection:

if (document.getElementById && document.createElement) {

If this is a modern browser, create a BUTTON node

	var butt = document.createElement('BUTTON');

and its text

	var buttext = document.createTextNode('Ready!');

Append the text to the button so that it is shown

	butt.appendChild(buttext);

Finally add an onClick event that calls the function saveEdit().

	butt.onclick = saveEdit;
}

The button now resides in the variable butt. We can call it if we need it.

P to TEXTAREA

Later on we'll define a general onClick event for the entire pages. All these events are sent to the function catchIt().

function catchIt(e) {

First of all, check if the user is currently editing a paragraph. If he is (editing is true) end the function.

	if (editing) return;

Support detection.

	if (!document.getElementById || !document.createElement) return;

Then we find the target of the event,

	if (!e) var obj = window.event.srcElement;
	else var obj = e.target;

Now we have the target, but a problem is that Mozilla considers the text node (and not the containing P) to be the target. Therefore we go up through the DOM tree as long as the current node is not a tag (as long as its nodeType is not 1).

	while (obj.nodeType != 1) {
		obj = obj.parentNode;
	}

Now we have ended up with a tag. If this is the textarea tag the user has clicked in the edit box to edit the text. If it's a link, it should be clickable as usual. In these cases nothing should happen so we end the function.

	if (obj.tagName == 'TEXTAREA' || obj.tagName == 'A') return;

Once again we go up through the DOM tree to find either a P or the HTML tag as the ultimate ancestor of the target of the click.

	while (obj.nodeName != 'P' && obj.nodeName != 'HTML') {
		obj = obj.parentNode;
	}

If we find the HTML tag the user has clicked outside a paragraph and nothing should happen. We end the function.

	if (obj.nodeName == 'HTML') return;

After this check we finally are certain that the user has clicked on a paragraph and that he wants to edit it. Take the innerHTML of the paragraph and store it.

	var x = obj.innerHTML;

Create a new TEXTAREA element and store it.

	var y = document.createElement('TEXTAREA');

Then take the parent node of the paragraph.

	var z = obj.parentNode;

The situation is now:

                        z
                        |
    ---------------------------------------
    |                   |                 |
  [more]                P               [more]

Insert the new TEXTAREA just before the paragraph in its parent node.

	z.insertBefore(y,obj);

Do the same for the 'Ready' button.

	z.insertBefore(butt,obj);

Now the tree looks like this:

                        z
                        |
    ---------------------------------------
    |       |           |           |     |
  [more] y(TEXTAREA)  butt(BUTTON)  P   [more]

Remove the paragraph itself. Now it seems as if the textarea and button have taken the place of the paragraph.

	z.removeChild(obj);

Only now, after inserting it, we can put the inner HTML of the P in the TEXTAREA. Doing this before insertion is impossible in Mozilla.

	y.value = x;

Put the focus on the textarea for user convenience

	y.focus();

and set editing to true: user is busy editing.

	editing = true;
}

TEXTAREA to P

When the user clicks on the 'Ready' button we should do the reverse. The function saveEdit() takes care of everything.

function saveEdit() {

Get the TEXTAREA (this assumes there is only one such field in the entire page).

	var area = document.getElementsByTagName('TEXTAREA')[0]

Create a new paragraph and store it.

	var y = document.createElement('P');

Find the parent node of the textarea: the paragraph should be appended to it.

	var z = area.parentNode;

Set the innerHTML of the new paragraph to the value of the textarea.

	y.innerHTML = area.value;

Then insert the new paragraph just before the textarea.

	z.insertBefore(y,area);

Remove the textarea.

	z.removeChild(area);

Remove the 'Ready' button (again, this assumes there is only one button on the page).

	z.removeChild(document.getElementsByTagName('button')[0]);

Finally, set editing to false: the user has stopped editing.

	editing = false;
}

Event

Outside and after the functions, set a general onclick event for the entire page calling the function catchIt():

document.onclick = catchIt;