Category: code

I just placed an order through our website and had an issue with the state. The customer lives in Oregon, so I typed “O” hoping Oregon would auto populate, but it didn’t. I was stuck having to scroll through all 50 states to find Oregon.

Confusing for most people.

I tried it, and it’s true. Typing “o” doesn’t narrow the list down much, and the way it does narrow it down doesn’t make much sense in this context: The autosuggest on the State field matches the search phrase anywhere in one of the options, so typing “o” gets you a list of all states with the letter “o” in the state name, ordered alphabetically by state name. Go ahead and try it out in a WooCommerce demo shop.

Out of the box WooCommerce uses SelectWoo, which is essentially Select2 with more accessibility. Select2’s default behaviour of matching the typed text to any part of an option makes sense in many places, especially when searching a store or for categories, but for selecting states, (and countries, although slightly less), it’s confusing.

Nothing’s filterable, but we can set a default

Select2 lets us set the callback function used for matching, so we can customize the matching behaviour, but there’s a problem, the settings for SelectWoo are set in WooCommerce’s country-select.js file, and there’s no way to filter them, so we can’t change the matching behaviour on initialization. It turns out that once a Select2/SelectWoo instance is initialized there’s no way to change the settings either.

When I got to here I thought we were out of luck – either I’d be creating a child theme and re-implementing the checkout, or we were going to live with SelectWoo’s default filtering behaviour. Luckily, I found one way to change the matching callback: It’s possible to set the default SelectWoo options callback before any instances are initialized. So that’s what we’re going to do.

Server-Side

First we need to add a bit of Javascript to our checkout page, so we enqueue a small script that’s dependent on the selectWoo script.

And that’s all the PHP we need! Because our solution changes the default matching behaviour we are careful to only enqueue this script on the Checkout page using WooCommerce’s is_checkout() function.

Client-Side

The enqueued Javascript file is pretty simple as well:

(function($) {
// Based on custom matcher here: https://select2.org/searching#customizing-how-results-are-matched
function start_matcher(params, data ) {
// If there are no search terms, return all of the data
if ($.trim(params.term) === '') {
return data;
}
// Do not display the item if there is no 'text' property
if (typeof data.text === 'undefined') {
return null;
}
// `params.term` should be the term that is used for searching
// `data.text` is the text that is displayed for the data object
// Make sure to compare in the same case!
if ( data.text.toLowerCase().indexOf(params.term.toLowerCase() ) === 0 ) {
return data;
}
// Return `null` if the term should not be displayed
return null;
}
$.fn.select2.defaults.set('matcher', start_matcher );
})(jQuery);

There’s a closure so we don’t pollute the global namespace, then we define a new matcher function that matches the beginning of the phrase, and set it as the default matcher. This code runs as soon as it’s ready – it doesn’t wait for a DOMReady event, because by the time DOMReady fires the SelectWoo boxes have probably been initialized already.

Conclusion

I wish we didn’t have to set the defaults for the whole page, but that’s what we’re stuck with at the moment. We could apply different matching patterns to different pages by adding a few more conditions to the PHP block, and a bit more Javascript. This works to make the checkout page work as expected, though.

All the code is up on Github as a WordPress plugin. If it’s useful let me know!

This week a client asked to add estimated ship times to the single-product pages in WooCommerce. Ship times are sometimes different for different products, so adding it as a custom field to each product made sense. As a bonus, there’s a “Shipping” tab in the Product Data metabox that the field can go in. Some Googling led me to Tom McFarlin‘s “Adding Custom Fields to Simple Products with WooCommerce” tutorial on tuts+, which is pretty thorough, but out of date – the actions he’s are no longer in WooCommerce, so it’s time for a new version of that tutorial, my-style. There are three main tasks to accomplish: Add the field to the WordPress back-end, save whatever is in the field, and output the data on the Single Product page. We’ll look at each task separately. The completed plugin is up on GitHub if you want to follow along.

Add the Custom Field to WooCommerce in the WordPress Back-End

WooCommerce provides an incredible number of hooks and filters. We’re going to us woocommerce_product_options_shipping hook, because it runs in the shipping block, but is an equivalent hook for each Product Data tab. Look at the files in the woocommerce/includes/admin/meta-boxes/views directory to understand how the Product Data, (and all the WooCommerce Metaboxes), are created, and what hooks are available.

We make sure we have the $product_object variable available, (it’s set in the WC_Meta_Box_Product_Data class, linked with the @see directive), then we make sure we’re adding the field to a “Simple” product type. Then, instead of writing a bunch of HTML, we set some info in an array, and pass it to the woocommerce_wp_text_input() function, (with a backslash, because the plugin is namespaced). WooCommerce has several of these helper fields available that create a form fields and their associated markup so they fit nicely with the rest of the WooCommerce admin.

Notice we’re already getting a value with get_post_meta(). That’s because the same code gets used every time the field is displayed – so if there’s a value in the database we want to display it. This is all we need to properly display a field in the Shipping section of the Product Data. We could add a name attribute to the $field variable, but it defaults to the value of id, so I left it out. Setting desc_tip to true puts the description in the tooltip that appears when someone hovers the circle with the question mark beside the field.

The last line of the code block above hooks our function into the woocommerce_product_options_shipping action. Because the plugin has its own namespace we need to include the full name of the function in the add_action() call.

Our new “Ships In” field, looking great.

Save Whatever is Entered in the Field

Again, a game of hooks figuring out how to save the data. Tom’s tutorial from last year used a woocommerce_process_product_meta hook which no longer exists. It looks like it’s been replaced by a woocommerce_process_product_meta_* hook for each product type. Since we’re only using a Simple product, we’ll use the woocommerce_process_product_meta_simple hook. Now we know which hook to use, some code:

In the save() function, (which we can name “save” without collisions because we’re using namespaces), we check the field is part of the $_POST array, check the nonce set by WooCommerce, and save the contents of the field, without forgetting to sanitize the data first.

WooCommerce may already checks the nonce for us, but I am not 100% sure, so I added an extra check.

Output the Data on the Single Product Page

WooCommerce’s plethora of hooks make placing the output relatively simple. I chose to put it right below the “Add to Cart” button on the Single Product page, but if you look through WooCommerce’s templates/content-single-product.php file and the files in the templates/single-product directory you’ll find lots hooks to use – and of course you’re not restricted to the single-product page. If you want to add your output to each product in a list of products take a look in the templates/content-product.php file.

Our print_output() function is pretty simple: Grab the post_meta, check it’s not empty, and output it mixed in with a “Ships In” string. It’s hooked into the woocommerce_single_product_summary action with a priority of 45, which means it appear after the Add to Cart button and after the Product Meta. WooCommerce’s template files are really good at explaining what functions are hooked to actions, and the priority they’re hooked at, which makes it easy to place things on a page without having to copy templates over to your theme.

Here you can see the output of the new “Ships In” field on the single-product page. The product meta was moved elsewhere.

Conclusion

Adding fields to the WooCommerce part of a WordPress admin area is much easier than adding a whole metabox. You can grab the completed plugin on Github. You’ll see there’s a bit more to the plugin: requirements are checked using Mark Jaquith’s method before embarking on a namespaced plugin with shorthand array syntax.

If this is useful let me know, and feel free to open tickets and send pull requests on Github.

If, like me, you use PHP’s Opcache to speed up a site, and you have a Varnish cache, and maybe some other server-side caches, you probably want to flush the opcache, Varnish cache, and any other server-side caches at the same time after changing the PHP files on your server, (like, when you update WordPress, plugins, or themes). I don’t want to click a button for each cache type, or worse, have to log in to the command-line for a sudo service restart varnish after doing updates, especially if I’m doing repeated updates. Flush Opcache with Varnish hooks into Mika Epstein’s excellent Varnish HTTP Cache plugin and flushes the PHP Opcache and the WP Super Cache cache, (if you use WP Super Cache), every time you manually flush the Varnish cache, turning the Varnish HTTP Cache “Clear Cache” buttons into a three-for-one deal.

If you also use the plugin WP Opcache to manage your opcache then WP Opcache will be called to do the actual Opcache flushing so you can take advantage of its automatic rebuilding of the opcache.

I’m already using Flush Opcache with Varnish in production and it’s saving me time every time I update a theme or plugin. You can get it in the WordPress.org plugin directory.

The Problem

Earlier this fall I was working on an e-commerce site that’s hosted on WPEngine that used Themeco’s Pro as its parent theme most pages hadn’t seen any customization yet, most of the code was stock, and this appeared on the Products page:

They’re hooking into the wp_calculate_image_srcset filter and returning false, which stops WordPress from adding a srcset attribute to an image, (more on why in a moment). The problem here is that the value being passed to the filter, $sources, is an array, but x_disable_wp_image_srcset() turns that array into a boolean false, so then anything else hooking into wp_calculate_image_srcset, for example, WPEngine’s CDN system, tries to iterate over the $sources array, but it’s not an array, hence the error. By filtering $sources to a boolean false Themeco is breaking the expected, and documented, behaviour of the wp_calculate_image_srcset filter.

Solutions

Luckily, there are two easy fixes for this problem. The first is to make sure you’re using the latest version of WPEngine’s must-use plugin. They’ve caught on to the problem and check to make sure $sources is actually an array:

If you can’t do that, at least Themeco made the function pluggable, so you can add your own version of x_disable_wp_image_srcset() to your theme’s functions.php or a plugin that returns an empty array and plays nicely with other plugins:

// This is a pluggable function. If we don't define it here then Pro does in
// pro/framework/functions/global/admin/thumbnails/setup.php
function x_disable_wp_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
return [];
}

Hey John,
Thanks for writing in. In looking over WordPress' official documentation for that function/hook, I believe that boolean false should be the correct value to return:
https://developer.wordpress.org/reference/functions/wp_calculate_image_srcset/
In the "Return" section you'll see that it is supposed to return a string or a boolean false if there is an "error or only one source exists." It seems as though WPEngine should need to revise their function to account for these instances anyway even if not for X or Pro (and at least work to catch any fatal errors like this).
Hopefully that helps, cheers.
Best Regards,
Your Themeco Team

This confusion occurs because the convention in WordPress is that if a filter and a function have the same name, the filter filters the output of the function. However, in this case the convention is broken – when I raised the issue in the Advanced WordPress Facebook group a very experienced WP developer made the exact same mistake and assumed that allowable return types for the function should be fine to return from the filter. After we cleared up the confusion I filed a bug about the confusing names, go star it so it gets some attention!

A Fluke of PHP

So why does returning false in the wp_calculate_image_srcset filter stop WordPress from using responsive images? Here’s the relevant code from media.php:

To follow along, if $sources is turned into a boolean false at the apply_filters() call, then a count( false ) happens in the condition on the next non-comment line. Instead of an error like I expected, running count() on any non-array value returns 1, so count( false ) == 1, also count( 0 ) == 1

Conclusion

First, be a good citizen of the WordPress ecosystem and don’t change the type of a filtered value to something other than the documented types for that value. Second, if you have any non-core themes or plugins installed you can’t trust that a value coming into a filter will be the type that the docs say, so check! When WPEngine realized there was a problem an is_array() fixed the problem. Third, read support E-mails thoroughly and make sure you understand them, it hurts when I go to the effort to document a problem and come up with a solution, and I’m told it’s not a problem. Finally, go +1 my ticket so maybe others won’t get bitten.

I’ve been working on a website where we display YouTube videos in a modal overlay. This seems simple: absolutely position a div and centre YouTube’s iframe embed within it. When the user tires of the video remove the iframe and hide or remove the div. It works great, then I fired up Internet Explorer for testing.

In IE this is what appears in the Javascript console, after the iframe is removed:

These errors appear after html5player.js shouldn’t exist anymore.

With the iframe gone html5player.js and its code shouldn’t exist anymore and therefore should not throw errors. It turns out that when an iframe is removed from a web page IE doesn’t completely purge the contents of the iframe from memory right away, if ever.

How do we fix it?

The errors occurred when using jQuery to set the innerHTML of the containing div to an empty string, so I started trying to explicitly remove the iframe’s DOM node using pure Javascript. Element.removeChild() the node instead of destroying it, so that didn’t kill the errors. The docs for IE’s removeNode() don’t specify if the node is held in memory, but the errors continued with it as well.

Some Googling turned up, (I forget where), that setting the iframe’s src attribute to an empty string might help. This makes sense because it’s the equivalent of navigating the iframe to a new page, which should unload any Javascript. Trying to set src=” worked, most of the time. When it didn’t work IE went crazy and became unusable.

In the end I took the approach of trying to wipe out the contents of the iframe as thoroughly as possible, first by changing the iframe’s src to an empty string, then by accessing the iframe’s DOM with Javascript and setting its document element to null. This did the trick. Here’s the function:

// note: modal is the div that contains the YouTube iframe
// jquery is used for some selecting, but the guts of the removal are pure Javascript
function purgeVideo() {
// purge video must be called while the video is still displayed on the page,
// so doing a jQuery fadeOut(purgeVideo) may throw errors, (I think because
// fadeOut sets display:none before calling its callback).
if( modal ) {
// get the iframe, declare variables
var ifr = $('iframe', modal), rifr;
// if the iframe is found get the underlying DOM node from jQuery
if ( ifr.length > 0 ) {
rifr = ifr.get(0);
}
// make extra sure the DOM node exists before starting to work on it.
if( rifr ) {
// set src to '' to start removing YouTube and get around cross-domain access restrictions
rifr.src = '';
// now set the document element of the iframe to null, (I think this is IE-specific)
if ( rifr.documentWindow ) {
rifr.documentWindow.document = null;
rifr.documentWindow = null;
}
// delete the iframe, (also removes it from DOM).
delete rifr;
}
// this won't throw an error, even if the iframe is gone, (we're back to jQuery here).
ifr.remove();
// remove anything else, (titles, etc), from the modal overlay
modal.html('');
}
}