I was first introduced to Plupload when I was building websites in .Net. I had some great HTML5 file upload tools that worked wonders in my browser of choice, but most of my colleagues (and about 80% of our clients) were using a browser that didn’t support the API. I used Plupload as a reliable cross-browser tool that would use HTML5 where available and fall back on Silverlight for those users who hadn’t yet upgraded to a real browser.
Cooler still, WordPress incorporated Plupload in to core! Now I could use the same tool for both my .Net projects and my open-source WordPress projects. I already knew the API forwards and backwards, so I was set.
Until the first time I tried to manipulate a Plupload event in WordPress.
Hacking Through The Brush
Like many other good JavaScript projects, Plupload doesn’t litter the global namespace with a lot of meaningless crud. Unfortunately, this means that unless you store a reference to your shiny new Uploader instance, you won’t be able to talk to it again.
In the 3.4 days, WordPress built out a bunch of handlers for the various Plupload events. Then they buried the Uploader instance in a closure-scoped variable.
Inside an iFrame.
Needless to say, overriding or augmenting any of the stock upload events became somewhat impossible. As a matter of fact, I stated so in response to a WordPress Stack Exchange question about manipulating these events just over a year ago.
Finally, a Solution
A project I’m working on right now is still using the 3.4-style media uploader.[ref]The reasons for this are beyond the scope of this post.[/ref] After implementing the uploader, my client made a request to lock down file types allowed in the upload to images only. Conveniently, Plupload gives us a FilesAdded event where we can hook in to scan the newly added file and either allow it or yell at the user.
Unfortunately, this event is assigned to the closure-scoped upLoader variable we referenced above. Furthermore, Plupload and its supporting scripts are being loaded inside an iFrame on the page, making it difficult to hook in at all.
Luckily, though, the FilesAdded event is merely a wrapper for a globally-scoped (in terms of the iFrame, anyway) fileQueued function. Using a few clever hacks, we can intercept this fileQueued function, replace it with one of our own, and keep chugging merrily along.
/**
* Object to encapsulate ThickBox-powered image uploads.
*
* @constructor
*
* @param {String} label ThickBox header text
* @parap {Object} picker jQuery double-click target
*/
function ImagePicker( label, picker ) {
var SELF = this,
$picker = $( picker ),
editor_store, ifWindow, fileQueued;
/**
* Handle the selection of an image.
*
* @param {String} html
*/
var send_handler = function( html ) {
var imagePath = jQuery( html ).attr( 'href' );
// Remove the modal overlay
window.tb_remove();
// Restore original handlers
window.send_to_editor = editor_store;
ifWindow.fileQueued = fileQueued;
// Trigger internal change event
SELF.changed( imagePath );
};
/**
* Our custom click event handler
*
* @param {Event} e
*/
var launchOverlay = function( e ) {
e.preventDefault();
editor_store = window.send_to_editor;
// Set up our new handler
window.send_to_editor = send_handler;
// Show the overlay
window.tb_show( label, 'media-upload.php?TB_iframe=1&width=640&height=263' );
$( document.getElementById( 'TB_iframeContent' ) ).load( intercept_image_files );
};
/**
* Intercept the file queueing process for plupload and block non-images.
*/
function intercept_image_files() {
ifWindow = document.getElementById('TB_iframeContent').contentWindow;
fileQueued = ifWindow.fileQueued;
// Set new fileQueued handler
ifWindow.fileQueued = function( fileObj ) {
// If we're good, go for it!
if ( SELF.isImage( fileObj.name ) ) {
fileQueued( fileObj );
return;
}
// If we've gotten this far, someone's trying to do something nasty. Stop them!
window.alert( 'Sorry, that file doesn\'t appear to be an image.' );
};
}
/**
* Override this function to do some magic after an image is selected.
*
* @param {String} newUri The new image URL.
*
* @return {Boolean}
*/
SELF.changed = function( newUri ) {
return false;
};
$picker.on( 'dblclick', launchOverlay );
};
/**
* Check if a file name is that of an image.
*
* @param {string} src
* @returns {boolean}
*/
ImagePicker.prototype.isImage = function( src ) {
var ext = src.split( '.' ).pop();
ext = ext.toLowerCase();
var extensions = [ 'jpg', 'jpeg', 'png', 'gif' ];
for ( var i = 0; i < extensions.length; i++ ) { var extension = extensions[i]; if ( ext === extension ) { return true; } } return false; }; var picker = new ImagePicker( 'Set Background Image', document.getElementById( 'image-picker' ) );
Without going in to too much detail, the object above sets up a new ThickBox image picker. When you double-click the specified element on the page, an overlay will launch containing the media upload UI. When you select an image, a custom change handler is called so you can do whatever you need to with the path of the newly-uploaded image. To allow this custom handling, we have to store the original window.send_to_editor function somewhere, replace it with our own handler, then restore the original when closing the dialog.
Handling the FilesAdded event is similar. After the iFrame loads, we grab a reference to its internal window scope, grab the original fileQueued function so we can store it somewhere, replace it with our own handler, and restore it when the dialog closes.
In Summary
As one of my coworkers put it:
Can't is a four-letter word.
The task of hacking around a somewhat closed API seemed daunting at first. Even now, the fact that it took me over a year to figure out this workaround is frustrating. Luckily, the new media uploader introduced with WordPress 3.5 is a little easier to play with in terms of hooking events. It actually exports the Plupload instance as part of the wp.Uploader object, so you can hook to events natively without such hacky workarounds.
Still, it's nice to know that, even in the absence of advanced event management, it's possible to hook in to WordPress when you need to manipulate stock behavior. This flexibility is what continues to make WordPress a stellar platform for application development.