In my previous two posts I showed you how I’ve been attempting to implement FCKeditor with AJAX to get that all elusive edit-in-place functionality going. Now I’m going to reveal the final code.
The Content Problem
I use a topic map with xhtml content in the xml. When I generate content I automatically add titles and alt attributes using an xsl stylesheet. The problem is that, obviously, the content has extra bits inside it that I don’t want for editing. So, my solution requires me to fetch the raw content - without title or alt attributes, etc
If you don’t tweak your content out of a database for display, then you could just use the innerHtml method to grab the existing content from your page.
The Solution
First, download the file
You can download the javascript I use for my FCKeditor solution from here: fn_ajaxstuffjs.jpg
Make sure that you change the file extension (using a jpg file extension was the only way to get it here onto WordPress).
Second, stick it into your html file
<head>
<title>Matthew’s page</head>
<script src=”fn_ajaxstuff.js” type=”text/javascript”></script>
</head>
Third, create your activation button
In an earlier version, I stuck the function to activate the editor on the <div>. I’ve got a few other features to stick onto my page, so I figured I would add an edit button. Of course, just move it back to the <div> if you want to use a Flickr-like click-edit feature.
Here’s what I’ve done:
<div id=”toolbar”>
<a href=”#” onkeypress=”javascript:edit(’matthew-hodgson’);”
onclick=”javascript:edit(’matthew-hodgson’);” class=”edit”
title=”edit this page”>
<span>Edit content</span>
</a>
<noscript>
This page is AJAX-enhanced for editing. You need to hava javascript enabled,
and an AJAX-compatible browser, in order to use some of the features on this
page.
</noscript>
</div>
Cheat stuff
I put some variables into the page and use javascript to make queries of that content to use for the database/xml lookups.
<div id=”endnotes”>
<p><span>itemID:</span><span id=”itemID”>matthew-hodgson</span></p>
<p><span>type:</span><span id=”type”>player</span></p>
</div>
It’s generated by the xsl and is handy to do debugging by just looking at the page. It means, though, I have a handy way of using some variables without the need for a form.
The widget
When the POST request is made to your widget all you need to do is to return the value of the content you’re saving. This content will then replace the existing content in the page.
If the widget gets stuck and there’s an error you need to work out some way of letting your AJAX routine know and what to do as a result.
Where does the content go?
In this example, I use two <div> to handle the content.
<div id=”story”>
<div id=”story-content”><p>Here is some content.</p><p>I love content.</p><p>Content is great</p></div>
</div>
The <div id=”story”> element simply gives me a reference point for the content and AJAX action.
It’s after last child of this <div> that the textarea gets created. If you want to see it actually being created then simply turn off the layer hides and style it with some nice big green lines (that’s what I did :)).
After I change the content and submit the AJAX form, I’ll receive a notice that the widget has processed the content. It will be in the form <div id=”story-content”>some stuff</div>. I then replace all of the insides of <div id=”story”> with this new content.
A note about Safari
Don’t forget that FCKeditor doesn’t work with Safari. If people are going to use your editor feature let them know. Maybe you could even provide them with an alternative? I’ve not done that here, but it’s a thought I might take onboard in a later version.
FCKeditor and Prototype
When I was first trying to get FCkeditor to work my searching lead me to Prototype. There’s a lot of questions about how to get Prototype to work with FCKeditor or TinyMCE but no action. I had a go at making it work and had to just give up after several weeks. I’m no Javascript or DOM guru. I can stumble around at best. So, let me just say, forget Prototype and FCKeditor.
But, there is hope!
FCkeditor and Prototype can work side-by-side so that you can have edit-in-place for headings or other parts that will really only require a text field, and use [instances of] FCKeditor for the WYSIWYG stuff.
In my implementation of FCKeditor I’ve got Prototype working on the same page for the heading (the name element that corresponds to the content in the database) and it works fine. Just keep in mind the overhead you’re going to have with all these javascripty bits.
Here’s the code in it’s full glory!
OK. Enough chatter. Let’s go with the code.
I’ve tried to make comments here and there to explain what’s going on. Make use of the comments area below the blog if you need help or have any questions or just want to say ‘hi’ or ‘thanks’.
If you’d like to see an example of this in action, just visit topicmaps.matthewhodgson.com
var itemID
function edit(someID) {
var agt = navigator.userAgent.toLowerCase();
if(agt.indexOf(”safari”) != -1) {
alert(’FCKedior does not work with Safari\n\nFor more information see http://www.fckeditor.net/safari’);
} else {
//alert(’I notice you\’re not using Safari. Good! We can make FCKeditor available to you!’);
itemID = someID;
makeRequest(’widget.php’, ‘mode=api&templateID=content&itemID=’+itemID, ‘loadContent’);
}
}
function setEditorValue(instanceName, text ) {
document.ajaxform.FCKeditor1.value = text;
}
function showContentValue() {
return FCKeditorAPI.GetInstance(’FCKeditor1′).GetXHTML(true);
}
function hideContentLayer(someID) {
var someLayer = document.getElementById(someID);
someLayer.style.display = ‘none’;
}
function showContentLayer(someID) {
var someLayer = document.getElementById(someID);
someLayer.style.display = ‘block’;
}
function replaceContentInLayer(id, content) {
var someLayer = document.getElementById(id);
someLayer.innerHTML = content;
}
function alertContents(http_request) {
if (http_request.readyState == 4) {
// everything is good, the response is received
if (http_request.status == 200) {
//alert(’test1:’+http_request.responseText);
return http_request.responseText;
} else if(http_request.status == 500) {
alert(’500 Internal Server Error\nThe server encountered an unexpected condition which prevented it from ‘+
‘fulfilling the request. ‘);
} else if(http_request.status == 503) {
alert(’The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. ‘+
‘The implication is that this is a temporary condition which will be alleviated after some delay. If known, the length of’+
‘ the delay MAY be indicated in a Retry-After header. If no Retry-After is given, the client SHOULD handle the ‘+
‘ response as it would for a 500 response.’);
} else if(http_request.status == 504) {
alert(’The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server ‘+
’specified by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server (e.g. DNS) it needed to access in ‘+
‘attempting to complete the request. ‘);
} else {
//you can find more errors messages at:
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
alert(’There was a problem with the request. Error code=’+ http_request.status +’.');
}
} else {
// still not ready
//alert(’still not ready… waiting to receive msg from widget’);
}
}
function makeRequest(url, parameters, passType) {
var http_request = false;
if (window.XMLHttpRequest) { // Mozilla, Safari, …
http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
http_request.overrideMimeType(’text/xml’);
// See note below about this line
}
} else if (window.ActiveXObject) { // IE
try {
http_request = new ActiveXObject(”MSXML4.XMLHTTP”);
} catch (e) {
try {
http_request = new ActiveXObject(”Microsoft.XMLHTTP”);
} catch (e) {}
}
}
if (!http_request) {
alert(’Giving up
Cannot create an XMLHTTP instance’);
return false;
}
http_request.onreadystatechange = function() {
var strText = alertContents(http_request);
if (strText == undefined) {
//do something … anything you like …
//someMsg = document.createTextNode(’.');
//document.getElementById(’story’).appendChild(someMsg);
} else {
if(passType == ‘loadContent’) {
//dont want anyone doing any special actions now, do we!
hideContentLayer(’toolbar’);
//need a reference point for these new layers?
var targetLayer = document.getElementById(’story’);
//create a msg layer
var msgLayer = document.createElement(’div’);
msgLayer.setAttribute(’id’, ‘msgLayer’);
//stick the msg layer before the first child of the story layer
//this is above where the story-content layer exists
targetLayer.insertBefore(msgLayer, targetLayer.firstChild);
//create some text for our msg layer
//create a textarea layer
var textarea = document.createElement(’textarea’);
textarea.setAttribute(’id’, ‘FCKeditor1′);
textarea.setAttribute(’name’, ‘FCKeditor1′);
//create a submit button
var someSubmitButton = document.createElement(’button’);
someSubmitButton.setAttribute(’id’, ’submitAjaxForm’);
someSubmitButton.setAttribute(’name’, ’submitAjaxForm’);
someSubmitButton.setAttribute(’value’, “Submit”);
//someSubmitButton.addEventListener(’click’, submitFCKform, false);
if(typeof someSubmitButton.addEventListener != ‘undefined’) {
someSubmitButton.addEventListener(’click’, submitFCKform, false);
}
else if(typeof someSubmitButton.addEventListener != ‘undefined’) {
someSubmitButton.addEventListener(’click’, submitFCKform, false);
}
else if(typeof someSubmitButton.attachEvent != ‘undefined’) {
someSubmitButton.attachEvent(’onclick’, submitFCKform);
}
//create a cancel button
var someCancelButton = document.createElement(’button’);
someCancelButton.setAttribute(’id’, ‘cancelAjaxForm’);
someCancelButton.setAttribute(’name’, ‘cancelAjaxForm’);
someCancelButton.setAttribute(’value’, “Cancel”);
if(typeof someCancelButton.addEventListener != ‘undefined’) {
someCancelButton.addEventListener(’click’, cancelFCKform, false);
}
else if(typeof someCancelButton.addEventListener != ‘undefined’) {
someCancelButton.addEventListener(’click’, cancelFCKform, false);
}
else if(typeof someCancelButton.attachEvent != ‘undefined’) {
someCancelButton.attachEvent(’onclick’, cancelFCKform);
}
//where we gonna put the textarea and the buttons?
targetLayer.appendChild(textarea);
//targetLayer.parentNode.appendChild(someSubmitButton);
//targetLayer.parentNode.appendChild(someCancelButton);
targetLayer.appendChild(someSubmitButton);
targetLayer.appendChild(someCancelButton);
//set the value of the textarea
textarea.value = strText;
//create fckeditor
oFCKeditor = new FCKeditor(’FCKeditor1′);
oFCKeditor.BasePath = ‘/FCKeditor/’ ;
oFCKeditor.ReplaceTextarea();
//now hide the original text layer
hideContentLayer(’story-content’);
//hide the loading msg info
hideContentLayer(’msgLayer’);
} else if (passType == ‘replaceContent’) {
replaceContentInLayer(’story’, strText);
} else {
alert(’Error: No action has been specified for the call to the widget’);
}
}
};
http_request.open(’POST’, url, true);
http_request.setRequestHeader(”Content-type”, “application/x-www-form-urlencoded”);
http_request.setRequestHeader(”Content-length”, parameters.length);
http_request.setRequestHeader(”Connection”, “close”);
http_request.send(parameters);
}
function submitFCKform() {
var txtContent = showContentValue();
if(txtContent.indexOf(’<div id=”story-content”>’) != 0) {
txtContent = ‘<div id=”story-content”>’+ txtContent +’</div>’;
}
txtContent = escape(txtContent);
replaceContentInLayer (’story’, ‘Saving…’);
makeRequest(’http://widget.php’, ‘api=update&itemID=’+itemID+’&FCKeditor1=’+txtContent, ‘replaceContent’);
showContentLayer(’toolbar’);
}
function cancelFCKform() {
//return to the default state
window.location = ‘widget.php?itemID=’+itemID+’&templateID=people’;
}
See my example in action here >>