September 18, 2012, 6:39 am
8 :: Uploading Images with WMD Editor
If you're like me, you use the wmd-editor to publish your blog posts. Unfortunately, by default, the Insert Image functionality only accepts URLs and does not allow image uploads like StackOverflow does. Today, I hacked it, and I want to show you how I did it. Please use this as you see fit!

The following hack requires use of jQuery, jQuery UI, and Mike Alsup's jQuery Form Plugin for performing AJAX file uploads . Mike's plugin is a fantastic piece of software and rids developers of the headaches associated with transferring files to the server using AJAX. Go download a copy on github.
Obviously, in your <head>, you'll need to include the dependencies:
<script type='text/javascript' src='jquery.min.js'></script>
<link href='theme/jquery-ui.css' rel='stylesheet' type='text/css' />
<script type='text/javascript' src='jquery-ui.js'></script>
<script type='text/javascript' src='wmd/showdown.js'></script>
<script type='text/javascript' src='wmd/wmd.js'></script>
<link type='text/css' rel='stylesheet' href='wmd/wmd.css'/>
<script type='text/javascript' src='jquery.form.js'></script>
Lastly, we're going to be overriding some of the wmd defaults and writing our own code. Let's create wmd-image-fix.js for that. So also include:
<script type='text/javascript' src='wmd-image-fix.js'></script>
So with the dependencies out of the way, let's get started!
If you inspect the Image button
in the wmd editor with chrome dev tools, you'll find that it has an id of wmd-image-button. Likewise, if you inspect the Anchor button
, you'll find it has an id of wmd-link-button The basic idea is to attach a click handler to these buttons and fire our own custom events.
Why the anchor button too, you ask? If you inspect the wmd.js source, you'll find that both the anchor and image buttons both run off the same dialog-creating code. So what we do to one, will unfortunately affect the other. This has the consequence that if we're going to disable the default dialog for the image uploader and write our own, then we'll inadvertently screw up the anchor button in the process. Let's first get started writing wmd-image-fix.js and attach click handlers to both buttons.
$(document).ready(function(){
$("#wmd-image-button").live("click",function(){
//when the image button is clicked, this will fire.
});
$("#wmd-link-button").live("click", function(){
//when the anchor button is clicked, this will fire.
});
});
We use .live() because the wmd buttons are appended to the DOM dynamically and therefore, .click() won't cut it.
Regardless of which button is clicked, if you inspect the source, you'll find that both dialogs are given the class wmd-prompt-dialog. We can use this fact to enable the dialog if the anchor button is clicked, but disable it if the image button is clicked. Expanding on our code, we try:
$(document).ready(function(){
$("#wmd-image-button").live("click",function(){
$(".wmd-prompt-dialog").css({"opacity": "0", display: "none"});
});
$("#wmd-link-button").live("click", function(){
$(".wmd-prompt-dialog").css("opacity", "1");
});
});
But something seems to be the matter. When we refresh and click on the images, the code seems to simply not work. This is because we must wait for the dialog to be created and the modal to be applied by wmd.js. The standard way to wait is usually to receive a callback. But rather than hook right into wmd.js, let's just wait 100 milliseconds before attempting to hide or show the dialog:
$(document).ready(function(){
$("#wmd-image-button").live("click",function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css({"opacity": "0", display: "none"});
}, 100);
});
$("#wmd-link-button").live("click", function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css("opacity", "1");
}, 100);
});
});
Seems to work great: when the image button is clicked, we don't actually get a dialog anymore, and when the anchor button is clicked, we still do!
Next step is to actually create our very own jQuery UI dialog to display the ajax file upload UI. One could, create this dialog UI in HTML and hide it by default, but I've opted to keep everything inside wmd-image-fix.js, and dynamically construct the HTML with jQuery. The following code creates a wrapper div, encloses a form which in-turn encloses a file, textbox, and submit inputs. The dialog() function is finally called on the resulting div, causing our custom dialog to pop up. Remember, we only want to create it when the image button is clicked, not the anchor button!:
$(document).ready(function(){
$("#wmd-image-button").live("click",function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css({"opacity": "0", display: "none"});
}, 100);
var $div = $("<div>");
var $form = $("<form>").attr({action: "create_new_blog_post/submit_image", method: "post"})
var $file = $("<input/>").attr({type: "file", name: "image"});
var $name = $("<input/>").attr({type: "text", name: "name", placeholder: "Name"});
var $submit = $("<input/>").attr("type", "submit");
$form.append($name, $file, $submit);
$div.append($form).dialog({title: "Upload Image"});
});
$("#wmd-link-button").live("click", function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css("opacity", "1");
}, 100);
});
});
With a little CSS, my dialog looks a little like this:

not too bad huh?
We're just about finished of the front-end, it's time to actually use the jQuery Form Plugin to send the file to the server via AJAX when the form is submitted.
You see - wmd is smart, as you know, it keeps track of all your images and anchors by assigning an id to them and enclosing that id in square brackets. There is a substantial amount of code inside of wmd.js that manages these id's and which urls are assigned to them. We do not want to destroy this functionality, so what we will do is basically hijack it. We actually need to make a single change to wmd.js. Go on in there and search (ctrl+f) for var form = doc.createElement("form"); Immediately following this line, assign the form an id, dialogform will do: form.id = "dialogform";.
Now in our custom wmd-image-fix.js file, we'll be able to reference this default form and hand values off to it. By doing it this way, we'll not lose wmd's abilities to manage the id's and their associated urls.
We call the .ajaxForm() function and chain it to the $form variable we created earlier. The .ajaxForm() function takes a parameter which is a callback function when the AJAX request is complete. The callback takes a parameter, the response, which I've denoted as r. The response happens to be in the form of a string, so we first convert it to an object.
If the response is successful the default dialog's text input (which we've hidden previously with $(".wmd-prompt-dialog").css({"opacity": "0", display: "none"});! Will be filled with the value of the filename and the OK button will be clicked. This is what I mean by a hijack... Even though the default dialog was hidden, we still used it and gained all the internal goodies. Finally, our dialog is closed.
$(document).ready(function(){
$("#wmd-image-button").live("click",function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css({"opacity": "0", display: "none"});
}, 100);
var $div = $("<div>");
var $form = $("<form>").attr({action: "create_new_blog_post/submit_image", method: "post"})
var $file = $("<input/>").attr({type: "file", name: "image"});
var $name = $("<input/>").attr({type: "text", name: "name", placeholder: "Name"});
var $submit = $("<input/>").attr("type", "submit");
$form.append($name, $file, $submit)
.ajaxForm(function(r) {
r = $.parseJSON(r);
if(r.success){
$("#dialogform input[type='text']").val(r.filename);
$("#dialogform input[value='OK']").trigger("click");
$div.dialog("close");
}
});
$div.append($form).dialog({title: "Upload Image"});
});
$("#wmd-link-button").live("click", function(){
setTimeout(function(){
$(".wmd-prompt-dialog").css("opacity", "1");
}, 100);
});
});
That's really it for the client-side. Next, we'll write the server-side code to handle the incoming file, save it, and echo back the filename. Check this out:
function submit_image(){
$f = $_FILES['image'];
$p = $_POST;
$allowedTypes = array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF);
$detectedType = exif_imagetype($f['tmp_name']);
if(in_array($detectedType, $allowedTypes)){
$pi = pathinfo($f['name']);
$ext = $pi['extension'];
$target = "img/" . strtolower(str_replace(" ", "-", $p['name'])) . "." . $ext;
if(move_uploaded_file($f['tmp_name'], $target)){
$returnArr = array(
"success" => true,
"filename" => site_url($target)
);
echo json_encode($returnArr);
}
else echo json_encode(array("success" => false));
}
else echo json_encode(array("success" => false, "msg" => "Invalid File Type."));
}
Let's talk about what this function does. It first grabs the file send over by our custom form we created in jQuery and stores it in $f. It then stores all the $_POST variables in $p... which only happens to be the name we typed into the custom textbox. We then set up an $allowedTypes array so that only PNG's, JPEG's and GIF's can get through. We use the exif_imagetype() function to determine the image type submitted and compare it to these allowed types. If it's in the array we proceed, if it's not, we echo an error back to javascript.
If we proceed, the file extension is determined by first assiging pathinfo() to $pi and pulling the 'extension' out of it. The $target is then constructed by concatenating the img/ folder with the posted name (replacing spaces with hyphens and lowercasing it) and with the extension. If we are able to upload the file, we proceed, otherwise we echo an error back to javascript again.
If we proceed, the file was successfully moved to the /img directory and we can construct an array which includes "success" => true and the filename. It is this success flag that is caught in the ajaxForm() callback above. It is absolutely mandatory.
That's all folks
Love the dialect used by the wmd authors... wmd.ieRetardedClick. Genius.
♬ Guns and Cigarettes by Atmosphere on Lucy Ford
Godspeed.