Conditionally Loading Javascript & CSS with Fancybox for WordPress

The Problem

I’ve been working on improving the performance of a client’s WordPress-based website recently and it’s become very clear to me just how much CSS & Javascript plugins load, even when it’s not needed.

On this particular website 47% of the Javascript and 57% of the CSS loaded on the site’s homepage is not required on the homepage, but is required elsewhere, so it should be loaded conditionally. Sometimes this is easy, but sometimes it’s not.

Fancybox for WordPress is not an easy plugin to make load client-side resources, after all, it’s made to show a lightbox on any image, and especially on any gallery. When the wp_enqueue_scripts action happens, where it is recommended that we set the scripts and styles that will be used on a page, we don’t yet know what will be on the page. Fortunately, we can call the wp_enqueue_script() function after that, during the body of the page, and those scripts will be printed in the wp_footer() function. The same is true for wp_enqueue_style(). So, here’s what we do:

  1. Stop Fancybox for WordPress from including its scripts & styles by default.
  2. If a gallery or image that we want to use Fancybox on is displayed, we tell WordPress to display the Fancybox scripts & styles in the footer.
  3. Enjoy faster pageloads.

Sounds simple, but there are a few interesting bits. Now for the code.

The Code

I use three functions to check if the Fancybox scripts & styles are needed. They can be in a theme’s functions.php, (where I have them at the moment), or they could even be rolled into their own plugin.

First tell Fancybox not to include its code by default. Instead of manually dequeuing every style & script, remove the Fancybox functions that do the enqueuing from the wp_enqueue_scripts and wp_head action hooks:

// don't enqueue Fancybox scripts by default.
function jb_dequeue_fancybox_css_js() {
    remove_action( 'wp_enqueue_scripts', 'mfbfw_styles' );
    remove_action( 'wp_enqueue_scripts', 'mfbfw_scripts' );
    remove_action( 'wp_head', 'mfbfw_init' );
}
add_action( 'wp', 'jb_dequeue_fancybox_css_js' );

Next create a function that re-enables Fancybox, and remembers that it did so. Here there are two static variables, which will hold their value between function calls, (kind of like hidden globals), which store state. The $fancybox_is_used variable is returned so this function can be called with no arguments to find out if Fancybox has been used on page. Note the priority of 25 when hooking mfbfw_init() to the wp_footer action. This is needed because mfbfw_init() prints some Javascript to the page that relies on jQuery, and WordPress prints the enqueued scripts in the wp_footer action with a priority of 20, so mfbfw_init() needs to execute with a priority higher than 20.

function jb_fancybox_is_used( $used = false ) {

    // this is returned so we can call this function with no arguments to learn
    // if Fancybox has been used on a particular page.
    static $fancybox_is_used = false;

    // remember if Fancybox has been re-enabled already, so we don't enqueue the 
    // scripts multiple times
    static $fancybox_is_setup = false;

    if( $used === true ) {
        $fancybox_is_used = true;
    }

    if( $fancybox_is_used && ! $fancybox_is_setup ) {
        if( function_exists( 'mfbfw_styles' ) ) {
            mfbfw_styles();  // enqueue fancybox styles
            mfbfw_scripts(); // enqueue fancybox scripts
            // the 25 is important. WordPress prints footer scripts in the
            // wp_footer action with a priority of 20, and mfbfw_init() has to
            // be called after the footer scripts are already on the page.
            add_action( 'wp_footer', 'mfbfw_init', 25 ); 
        }
        $fancybox_is_setup = true;
    }

    return $fancybox_is_used;
}

Finally, make a function that looks for places where Fancybox is used on the page. On the site I’m working on the CSS class fancybox is used on images and in galleries that I want to be Fancyboxed, so I look for the string “fancybox” in the_content filter, and when it’s found I call jb_fancybox_is_used( true ) to re-enable Fancybox on the page. I added this to the_content with priority 11 because shortcodes, including gallery shortcodes, are executed at priority 10, and I want to be able to look through the output of short codes for the fancybox CSS class:

function jb_hunt_for_fancybox( $content ) {

    if( false !== stripos( $content, 'fancybox') || false !== stripos( $content, 'thickbox' ) ) {
        jb_fancybox_is_used( true );
    }
    return $content;
}
add_filter( 'the_content', 'jb_hunt_for_fancybox', 11 );

If you include a something you want to fancybox in a template you can call jb_fancybox_is_used( true ) manually from the template file to include the CSS & Javascript.

Other Ways

This isn’t the only way to conditionally include Fancybox’s Javascript & CSS. Instead of using jb_hunt_for_fancybox() to filter the_content there’s probably an action or filter in the gallery shortcode that jb_fancybox_is_used() could be hooked onto. It may even be possible to use the $wp_query object in an action hook just before wp_enqueue_scripts to determine if there is content on the page that needs to be Fancyboxed, let that decide whether or not to run jb_dequeue_fancybox_css_js(), and forget about the other two functions.

Let’s Do Better

Plugin authors should be working hard to only add what is needed to each page load. Who is doing a great job? How can we hack our themes to bend other plugins to our will? Comment or tweet @johnbeales to let me know.

Generating Form Elements with Javascript in IE 10

There’s a DOM manipulation gotcha in IE10 that just got me, and Google didn’t help much, so I’m giving Google something to show anyone who has this problem in the future.

When you dynamically generate a form input in Internet Explorer 10 the you must set the type attribute before doing anything else to the element, otherwise any values you set will be ignored when submitting the form, and in some cases will not be displayed. When inspecting elements using IE’s developer tools, however, the correct value appears in the generated document tree.

So this works as expected:

var sub = document.createElement('input');
sub.setAttribute('type', 'submit');
sub.setAttribute('value', 'Submit Generated Button');


But this doesn't:

var sub = document.createElement('input');
sub.setAttribute('value', 'Submit Generated Button');
sub.setAttribute('type', 'submit');

This is particularly hard to catch with radio buttons and checkboxes, (this is what got me). Their default value is “on” which doesn’t tell me much, especially if you’re submitting an array of them.

Here’s a demo. The first button shows its value, (and submits its value), and the second shows, (and submits) the default of “Submit Query.”

The only documentation I found on this behaviour is a passing sentence in the createElement documentation at MSDN. When they say “then set the type property to the appropriate value in the next line of code” they’re serious about the next line of code.

Also, who chooses “Submit Query” as a default value for a submit button in 2013 anyway? Are they trying to confuse people? Shouldn’t it just be “Submit”?

Improving Trac’s Tickets By Milestone Report

I entered a ton of tickets & milestones into a Trac installation today and when I was done the Active Tickets report was a mess. Tickets by Milestone was better, but still far from perfect.  Time for report customization. Google helped, and so did the #trac IRC channel. If you’re lazy & want to just jump to the solution, do it.

Here’s what I was looking for in my report:

  • Group tickets by Milestone
  • Order milestones by due date, (soonest first)
  • If a milestone had no due date, put it at the end of the report, (if it’s important it’ll have a due date set, otherwise it’s a “backlog” item that hasn’t been prioritized yet.
  • Display the due dates with the milestone names.

To get started, go to the Tickets by Milestone report that’s in Trac by default and click the “Copy Report” button, you’ll get a copy of Tickets by Milestone to play with. Click the Edit Report button and we’ll update the SQL to get the report we want. Grouping by Milestone is already done in this query, so we’ll start with ordering by milestone due date and putting milestones without a due date at the end of the report.

Order by Milestone Due Date

To order by date we need to join the milestone table. Add to the line after FROM ticket t:

LEFT JOIN milestone ms ON ms.name = t.milestone

Then to the beginning of the ORDER BY statement add (ms.due > 0) Desc,ms.due, so the ORDER BY is now:

ORDER BY (ms.due > 0) Desc,ms.due, (milestone IS NULL),milestone, CAST(p.value AS integer), t.type, time

The (ms.due > 0) Desc part makes milestones that have a due date come first, then ms.due orders those by due date with the soonest first.

Display Due Dates with Milestone Names

For Trac 0.12 and above replace the line

'Milestone '||milestone AS __group__,

with:

'Milestone '||(milestone || CASE WHEN ms.due > 0 THEN ', (due ' || datetime(ms.due/1000000, 'unixepoch')  || ' UTC)' ELSE '' END) AS __group__,

And for Trac versions below 0.12 replace the line with:

'Milestone '||(milestone || CASE WHEN ms.due > 0 THEN ', (due ' || datetime(ms.due, 'unixepoch')  || ' UTC)' ELSE '' END) AS __group__,

The difference is that in Trac 0.12 dates, (at least milestone due dates), started to be stored as mircoseconds since the unix epoch, and before that they were stored as a simple unix timestamp, so now, to use SQLite’s datetime function we have to divide the stored value by 1,000,000.

This statement makes milestone names look like this:

Milestone Page Style Updated, (due 2011-11-21 23:00:00 UTC)

Note that there’s a UTC time listed. This is because I can’t figure out how to get a user’s timezone offset preference into the query. It would be relatively simple if the time was attached to a ticket, but in this case it’s attached to a milestone. If anyone knows how to work the proper timezone offset into the SQLite query please let me know.

Bonus: Link the Milestone Titles to Reports Showing Only That Milestone

It’s possible to create a link a list of that milestone’s tickets. Just add this line after the line that you just altered:

(CASE WHEN(milestone IS NOT NULL) THEN '../query?group=status&milestone=' || milestone ELSE NULL END) AS __grouplink__,

The grouplink column is a magic column that Trac understands and uses as a link for the group title, (in this case, the milestones).

The Full Solution

For you lazy folks, here’s the full query:

SELECT p.value AS __color__,
   'Milestone '||(milestone || CASE WHEN ms.due > 0 THEN ', (due ' || datetime(ms.due/1000000, 'unixepoch')  || ' UTC)' ELSE '' END) AS __group__,
  (CASE WHEN(milestone IS NOT NULL) THEN '../query?group=status&milestone=' || milestone ELSE NULL END) AS __grouplink__,
   id AS ticket, summary, component, version, t.type AS type, 
   owner, status,
   time AS created,
   changetime AS _changetime, t.description AS _description,
   reporter AS _reporter
  FROM ticket t
  LEFT JOIN milestone ms ON ms.name = t.milestone
  LEFT JOIN enum p ON p.name = t.priority AND p.type = 'priority'
  WHERE status <> 'closed' 
  ORDER BY (ms.due > 0) Desc,ms.due, (milestone IS NULL),milestone, CAST(p.value AS integer), t.type, time

Transferring OS X and Boot Camp to a New Hard Drive

For Christmas my brother gave me a copy of the game Portal, which required there to be about four gigabytes of available space on my hard drive. There wasn’t. In fact, every time I’ve wanted to copy a large file for the last while I’ve had to re-arrange and purge my hard drive, both in OS X and in my Boot Camp partition. It was time for a new, upgraded, hard drive, and this is how I was able to copy both my OS X partition and Boot Camp, (Windows), partitions to the new drive, and expand both partitions to fill my drive, all without re-installing any software.

The Hardware

First, the hardware. I use a Macbook Pro that dates from the summer of 2007. Until I made this change it had its factory-installed 160 GB, 5400 RPM hard drive. This was an upgrade from the standard 120 GB drive, but three and a half years later it’s no longer big enough for me. I had dedicated 32 GB to my Boot Camp partition, which with a Vista installed was very cramped, perhaps even more cramped than my OS X partition that made up the balance of the drive. I ordered a 500GB, 7200 RPM Western Digital Scorpio Black , (yes, that’s an Amazon Affiliate link), hard drive from Amazon to replace the factory-installed drive. Apparently the WD Scorpio Black uses the same amount of power as a normal 5400 RPM drive, but is faster. I’m not super concerned with power these days as my 3 1/2 year old battery doesn’t exactly hold a charge so I’m always plugged in anyway.

A note on warranties and recalls:

My laptop is in the group affected by the NVIDIA Recall, so even though my 3-year AppleCare is expired, (and it was worth it – a new mainboard and hard drive later), I am still covered for a few months if the video goes kaput. I called Apple to see if I could change my hard drive without voiding that special coverage and they said that yes, so long as there was no physical damage to the computer, I would still be covered if my video died.

My Best Way to Transfer Everything, Step by Step

Required Equipment
  • Your Mac, with the old hard drive still installed
  • Your new hard drive
  • A way to connect your new hard drive to your Mac, probably a SATA to USB connector, or an external hard drive case.
Required Software
The Steps
  1. Plug your new hard drive into your Mac, using whatever connector you have.
  2. If your Mac isn’t already on, boot from your old hard drive
  3. Using Disk Utility format your new hard drive. Select a GUID Partition table, (so you can start your computer from the drive), and, unless you’ve specifically chosen another format, OS X Case-Insensitive, Journaled, as the format. Make the partition as one single partition, (volume), that fills the whole drive. We’ll add in a Boot Camp partition later.
  4. Using SuperDuper! copy your OS X partition from your old drive to the new volume you just created on your new hard drive. Use the “Backup – all files” option in SuperDuper!
  5. Go clean the garage, or plant the garden. This will take a while. It took about four hours for me to copy about 115 Gigabytes of data
  6. When SuperDuper! is finished its business shut off your computer and disconnect everything. You’re about to take your computer apart.
  7. Find your computer on iFixit.com and make sure you have the appropriate tools. I only needed two screwdrivers, however one of them was a T6 Torx screwdriver, and the smallest I had was a T8. My father-in-law also had a T8 as his smallest. We ended up using a file to give a hexagonal screwdriver a shape closer to a Torx screwdriver.
  8. Follow the instructions on iFixit.com to replace your computer’s old hard drive with the new one that you copied OS X to in steps 4 & 5.
  9. Once everything’s connected, but before you’ve put your whole computer back together, I recommend starting up your computer to make sure everything’s connected properly. Be careful not to touch anything inside your computer when it’s running, you could hurt yourself, (or worse, your computer!), if you touch the wrong thing. Once you know the hard drive is properly connected turn off your computer again and remove the power source.
  10. Re-assemble your computer.
  11. Hook your old hard drive up to your computer the same way you had the new hard drive hooked up before you installed it.
  12. Start your computer.
  13. If you did everything right you should be running off of your new hard drive now. Check that you are running off of your new hard drive by starting finder and checking the size of your hard drive, or use Disk Utility to check the brand name of your hard drive, or just start without the old drive hooked up and connected it later.
  14. Now we’re going to move your Boot Camp partition.
    1. With the computer booted, and the old hard drive connected externally, start Boot Camp Assistant, (it’s in Applications > Utilities).
    2. Follow the wizard to create a BootCamp partition. This partition does not need to be the same size as your old Boot Camp partition. When Boot Camp Assistant asks you to insert a Windows install disk quit Boot Camp Assistant. Your partition is created.
    3. Install and run WinClone. It will probably ask you to install the NTFSProgs Binaries, which it needs to do some reading and writing to NTFS-formatted filesystems, (like Windows partitions), these seem to be safe so go ahead and install them.
    4. With WinClone you’ll first need to make a disk image, (a file that contains the whole contents of your old Boot Camp partition), then restore it to your new Boot Camp partition. So, you’ll need OS X formatted space to store this image. This could be your new hard drive if you’ve just installed a larger drive like I did, or it could be another external drive.
    5. Tell WinClone to make an image of your old Boot Camp partition. It took about 1/2 hour for me to image a 32 Gigabyte partition.
    6. Tell WinClone to “restore” the data in the disk image you just made to your new Boot Camp partition. This could take a while. Grab lunch.
    7. When WinClone is done turn off your Mac and disconnect the old hard drive.
    8. Turn on your Mac holding down the Option key on the keyboard. You should see your Boot Camp partition as a boot option, (it’s probably labeled “Windows”). Select it to boot into Windows.
    9. Windows may want to run a chkdisk. It’s probably best to let it do so. It shouldn’t take crazy long, but will probably take long enough to make a pot of coffee.
    10. After chkdisk runs and you’re booted in Windows check everything is ok.
  15. That’s it. Enjoy your new hard drive!

Notes on Backups

The first time I connected my Time Machine drive to my Mac after doing the hard drive replacement Time Machine realized that I had installed a new hard drive and did a full backup. This took a while, (especially because I accidentally pulled the USB cable out of the computer halfway through). If you’re using a Time Capsule it is a good idea to plug your computer in to the Time Capsule with an ethernet cable, not do the full backup over the air.

BackBlaze, (again, that’s an affiliate link), which I use on two computers, didn’t notice the change in disks and continued as normal. I am pretty happy about that because the initial backup with any online service can take a long time and this saved me from uploading over 60 Gigabytes of data over my DSL connection.

Notes on Fragmentation

I took the opportunity to defragment both my Windows and OS X, (I use iDefrag to defragment OS X. In reality there was very little fragmentation on either side, I think that the process of copying everything from the old disk to the new one may have essentially defragmented everything anyway.

Running my Boot Camp partition in VMWare Fusion

The first time I tried to launch my Boot Camp partition in VMWare Fusion I got an error because the Boot Camp volume had changed. It asked me to remove and re-add the virtual hard drive, which I couldn’t figure out how to do in 5 seconds, so I removed my Boot Camp partition from my Virtual Machine Library. Then to re-add it I had to click “Home” in the VM Library window and choose “Run Windows from your Boot Camp partition” on the right hand side. There’s a setup that’ll run for a few minutes, (it took less than 2 minutes for me), and the Boot Camp partition should be re-added to the VM Library.

Windows Activation

After I had my Boot Camp partition running for a while in VMWare Fusion Windows informed me that it had been deactivated due to a hardware change and I had to reactivate. I don’t know if this was only because of the remove and re-add I did to the Boot Camp virtual machine, or if it was because of the actual hard drive change. Either way Windows had to be reactivated, which is a pain since activation online never works for me anymore and I always have to activate Windows over the phone. However, it’s activated now and seems to work fine.

Conclusion

While it seems like there were a lot of steps, and copying everything around took quite a while, it was much, much easier to copy everything from my old hard drive to a new one. I didn’t have to re-install any software or any operating systems, something that I was afraid I would have to do. It’s something that can be accomplished in about a day, if you have all of the tools and equipment on hand. If you do it on the weekend then you don’t have to feel guilty about the downtime.

Test ASP.NET apps on your Mac with VMWare Fusion & DD-WRT

Today I needed to test and fix mac-specific bugs in a website that is written in ASP.NET, but I generally use a Mac. When I work on the ASP.NET site I boot into Vista using Boot Camp, and the rest of the time I spend happily in OS X. I needed a solution to run both at the same time, and on the same computer, (this is the only mac I have, but my development environment for the .NET site is on my Boot Camp partition). It was time to see if Virtualization has gotten any better. It has.

There are two reasons that virtualization is better: VMWare Fusion 3.0 was released, and I doubled up my RAM, (now at 4GB). With these two changes, and Aero turned off on the Windows side, Vista runs ok under VMWare Fusion on my ageing Macbook Pro. Now to see changes made in Vista from OS X.

The simplest way I found to make this happen was to use some of the DHCP features DD-WRT on my router to assign a static IP address to my virtual machine and to map a URL to that IP. I’ll do this in steps:

  1. Before starting your VM in VMWare, go to the settings for the VM > Network and choose “Bridged” then open the advanced section and click the button to generate a MAC address. Copy the generated MAC address.
  2. To to Services > Services in your DD-WRT web interface and in the DHCP Server box assign a static IP to the Mac address you just copied
  3. In the DNSMasq box enter the following:address=/the-url-you-want-to-map-to-the-vm/THE.IP.YOU.JUST.SET entering, of course, the real values.
  4. Hit Apply and Save at the bottom of the page
  5. Start up your VM
  6. Make sure that Windows Firewall is set up to let HTTP connections through
  7. Type the URL you created in your browser in OS X and you should get the web page served by Windows
  8. Now, there should be a way to make this work without the router, using NAT network mode for the VM and some hosts file edits in OS X. I’m going to try to figure out how, but for now I just need to get some bugs fixed in the .NET app. If anyone has any ideas how to make this happen without involving a router let me know, (or blog about it and leave a note in the comments).