FCKeditor, AJAX and edit-in-place, (Part III) - The Final Frontier

3 February, 2007

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


FCKeditor, AJAX and edit-in-place, (Part II)

21 January, 2007

I had some success today getting FCKeditor to place nice with AJAX so that it will work for edit-in-line in the same way that Flickr works.

The trick with using FCKeditor and AJAX is that the content you want to edit is often whole paragraphs of text and markup. Grabbing the whole of that content straight from an existing <div> just doesn’t work because it is considered an object array by the DOM. What you’ll end up with when you grab the content is just the text - no markup.

So, as I indicated in my earlier post, you need to do the following:

  1. Dynamically create a textarea box and give it a name and an id
  2. Ask your database for the content as text, including all its markup
  3. Put the content into the textarea
  4. Create an instance of FCKeditor using the same name as the textarea
  5. Use oFCKeditor.ReplaceTextarea(); to swap the textarea with the FCKeditor

It’s important to be mindful of some posts around the place that suggest that, for Firefox to play nice with AJAX, you need to set the FCkeditor to null.

FCKeditorAPI = null;
__FCKeditorNS = null;

What I found was that it just made sure that the FCKeditor couldn’t be referenced once it was created (because it was null). So, I just removed it and it worked fine in Firefox.

So, enough with the commentary, let’s begin!

The main ajax functions are called by an onclick on the content:

<div id=”story” onclick=”javascript:fetchAndLoadFCKeditor(’http://mysite.com/form.php’, ‘content=myDatabaseID’,'FCKeditor1′ );”>

<!– begin content from database –>
<div id=”story-content”>
<p>Here is some nice content.</p>
<p>Isn’t it <a href=”nice.php”>nice</a></p>
</div>
<!– end content from database –>

</div>

I use AJAX to go fetch the content blob from a database via a little widget. It waits until the response and then uses the content returned to stick into the FCKeditor.

For fetch of content is particularly important for me because my content is in xml and, via xsl, the content I show on the page has special extra bits, like automated generation of title=”more information about”. I don’t want this to appear for editing, so I need to fetch a more plain version of the content for editing.

function fetchAndLoadFCKeditor(url, parameters, editorname) {
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 … replaceContentInLayer (’story’, ‘Loading…’);
} else {

//this is where the interesting stuff begins
// basically, we’re going to dynamically create content, including the FCKeditor

//first, we need a reference point for these new layers - <div id=”story”>
var targetLayer = document.getElementById(’story’);

//create a textarea layer ..note that editorname will be used again for the instance name
//for the FCKeditor
var textarea = document.createElement(’textarea’);
textarea.setAttribute(’id’, editorname);
textarea.setAttribute(’name’, editorname);

//create a submit button
var someSubmitButton = document.createElement(’button’);
someSubmitButton.setAttribute(’id’, ’submitAjaxForm’);
someSubmitButton.setAttribute(’name’, ’submitAjaxForm’);
someSubmitButton.setAttribute(’value’, ‘Submit’);
//this is a browser detect script because IE uses different event handlers than does FF

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’);

//again, use the browser detect script because IE uses different event stuffs than FF

if(typeof someSubmitButton.addEventListener != ‘undefined’) {
someCancelButton.addEventListener(’click’, cancelFCKform, false);
}
else if(typeof someSubmitButton.addEventListener != ‘undefined’) {
someCancelButton.addEventListener(’click’, cancelFCKform, false);
}
else if(typeof someSubmitButton.attachEvent != ‘undefined’) {
someCancelButton.attachEvent(’onclick’, cancelFCKform);
}

//we now use the <div id=”story”> layer as the target point for where
// we are gonna put the textarea
targetLayer.appendChild(textarea);
targetLayer.parentNode.appendChild(someSubmitButton);
targetLayer.parentNode.appendChild(someCancelButton);

//now we use the strText we grabbed to set the value of the textarea
textarea.value = strText;

//lastly, we create fckeditor
var oFCKeditor = new FCKeditor(editorname);
oFCKeditor.BasePath = ‘/somepath/FCKeditor/’ ;

//using the ReplaceTextarea() allows us to create the FCKeditor in the place where
// we dynamically created the textarea

oFCKeditor.ReplaceTextarea();

// now hide the original text layer - we want to see it vanish and the editing area
// seemingly replace it… just use a standard layer visibility=none routine

hideContentLayer(’story-content’);

}

};

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);

}

Now, all I have to do is build the routine to send off the content … save it into the database … and replace the content on this page.

You could reuse/tweak the above AJAX call so that it could send off the request for updating content to the database. The form itself should just echo a message (the content you sent) back to this page so that the response becomes the new content, replacing that inside <div id=”story”>

…I’ll leave that one til next time…

Read on to Part III >>

M


Export xml content into another xml file

16 January, 2007

I decided a long while ago that I should move the xhtml content inside my topic maps engine into their own separate files. What I found was that it was easier said than done.

I searched lots of forums, but unless you want to start using xslt 2.0 there’s not going to be an easy solution, particularly if you’re using MSXML, VBscript and ASP like I do. Luckily, I did find a way to do it.

There are a few tricks you need to learn.

  1. Use the microsoft xslt extension
  2. Create a script in your preferred language
  3. Call the script from inside the xsl stylesheet.

Here’s my example. It’s an xsl stylesheet I used to recently clean up a topic map:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version=”1.0″
xmlns:xsl=”http://www.w3.org/1999/XSL/Transform
xmlns:msxsl=”urn:schemas-microsoft-com:xslt”
xmlns:user=”urn:my-scripts”
xmlns=”http://www.w3.org/1999/xhtml” >
<xsl:output method=”xml” encoding=”ISO-8859-1″ omit-xml-declaration=”no” indent=”yes” xml:lang=”en” />

<xsl:param name=”category” />
<xsl:param name=”item” />
<xsl:param name=”author”/>
<xsl:param name=”error”/>
<msxsl:script language=”JScript” implements-prefix=”user”>
<![CDATA[
function writeTextContent(outputFile,nodes) {
var objFSO = new ActiveXObject("Scripting.FileSystemObject");
var objFile = objFSO.CreateTextFile(outputFile, true);
try {
for (var i = 0; i < nodes.length; i++) {
var sTmp = nodes.item(i).xml;
objFile.Write(sTmp);
}
}
finally {
objFile.Close();
delete objFSO;
delete objFile;
}
return("");
}
]]>
</msxsl:script>
...[SNIP]…


<!– this is the file name we want to use for storing information –>
<xsl:variable name=”this-id”>W:\webs\test1.magia3e.com\xml\<xsl:value-of select=”normalize-space(ancestor::node()/@id)”/>-content.xhtml</xsl:variable>

<xsl:value-of select=”user:writeTextContent(string($this-id), $this-content)”/>
<property type=”content” href=”{normalize-space(ancestor::node()/@id)}-content.xhtml” />
</xsl:if> I hope you find it useful. M