Category Archives: Tips

How To Create a Related Posts Slider / Carousel in WordPress

This is a very common task: to show related content under a given post. Surprisingly there is no such built-in function in WordPress so here are two ways to achieve it.

The Easy Way: With The Free Shortcode Revolution Plugin

See in this video:

The plugin can be downloaded from the official repository.

The DIY Way: Code

If you are here to just get the work done, use the plugin suggested above. If you are looking to learn and / or need a custom solution, let’s see the code. It’s pretty simple.

First, let’s figure out what would related mean? You don’t have to create a complicated AI. The related posts can be selected from posts from the same category and/or with the same tags.

Let’s dive in the code. You will need to create a function – it could be a function right in your theme, which will then be used in the single-post.php template, or (probably better) you can do it with a shortcode. I’ll leave this decision for you.

global $post; // this is the currently shown post on the page		
if(empty($post)) return ''; // just make sure you don't run this in the wrong place where $post may not exist

// let's prepare the WPQuery parameters
$query = [];	

// We will add both tags and category as criteria. Feel free to use only one of them
$tags = get_the_tags($post->ID);
if(empty($tags) or !is_array($tags)) $tags = [];
$tags_arr = [];
// we can't use $tags directly as they come from get_the_tags() because they are objects. So we are filling the new $tags_arr
foreach($tags as $tag) $tags_arr[] = $tag->name;						
$query['tag_slug__in'] = $tags_arr;

// similar for categories
$cats = get_the_category($post->ID);
$cats_arr = [];
foreach($cats as $cat) $cats_arr[] = $cat->term_id;
$query['category__in'] = $cats_arr;

// Let's say we want 5 random posts. We'll select 6 and then strip to 5 just to make sure the current post is not included.
// You could use the "post__not_in" attribute for this but it has some specific which make us prefer to clean the unnecessary post in PHP.
$query['posts_per_page'] = 6;
$query['orderby'] = 'rand';
$wp_query = new WP_Query($query);
$posts = $wp_query->posts;

// here we'll cleanup the current post with array_filter and a closure
$posts = array_filter($posts, function($p) use($post->ID) {				
    return ($p->ID != $post_id);					
});

if(5 < count($posts)) array_pop($posts);

That’s it. You’ll have the related posts in the $posts variable. If you want to learn more how WP_Query works, check here.

Watch out for some pitfalls if you’ll use $posts outside of the loop. Or just use WP_Query -> have_posts().

From then on, you can use any JS slider and prepare the required JS variables.

In Shortcode Revolution we preferred to build a vanilla JS slider using most of the code from this great guide.

Alternatively you can use aready jQuery carousel plguin like Slick.

 

Oddness in The WordPress Posts Loop and WP Query

While working on the new version of Shortcode Revolution we faced a couple of confusing behaviors of WordPress functions. I’ll document them here and will keep adding such information. Hopefully it can help other developers and save them a lot of time and frustration.

Excluding posts from WP Query (using post__not_in)

There is a good comment here explaining that post__not_in wouldn’t work when used with post__in in the same query. This is a good note but our query did not include post__in and post__not_in was still simply ignored. I did not dig deep into the code to see why this happens – the conclusion was simply not to use it.

Here is an example how we get 3 random related posts in Shortcode Revolution:

$query = [];
// we add 1 to the default number of posts which is 3 or to the user-passed number or posts
$query['posts_per_page'] = (empty($atts['num']) or !is_numeric($atts['num'])) ? 4 : intval($atts['num']) + 1;       
$query['orderby'] = 'rand';

// getting the posts here
$wp_query = new WP_Query($query);
$posts = $wp_query->posts;

// now because this is "related posts" query, we don't want the current post to be repeated below it. So let's filter it out
// $post_id is a variable we have defined earlier in the shortcode and it contains the current post ID
// we are using array_filter with a closure here. You can do it inside a loop with a counter but this solution is more elegant
$posts = array_filter($posts, function($p) use($post_id) {				
  return ($p->ID != $post_id);					
});

// if the current post was not there we have one more than needed
if($query['posts_per_page'] < count($posts)) array_pop($posts);

 

So that’s it – you just get more posts than you need and then remove the unwanted ones in PHP.

Showing the excerpt outside of the post loop – get_the_excerpt

If you look at the documentation it sounds like you can call get_the_excerpt with any post ID and receive the excerpt for that post id. Good luck! You may get the proper excerpt (sometimes!) but won’t get the automatically generated “read more” link properly. The first version of our code was this and it did not work:

foreach($posts as $p):
   $background_image =  has_post_thumbnail( $p->ID )  ?  get_the_post_thumbnail_url($p->ID) : ''; 
   $excerpt = get_the_excerpt($p->ID);
.....

I intentionally did not use $posts as $post to avoid overriding the global $post variable but apparently this was wrong. Fortunately this comment gave me the hint how to fix it:

foreach($posts as $p):
   // yes, do override the global!
   $post = $p;
   // and you need to setup_postdata($post)
   setup_postdata($post);
   $background_image =  has_post_thumbnail( $p->ID )  ?  get_the_post_thumbnail_url($p->ID) : ''; 
   $excerpt = get_the_excerpt($p->ID);
......
endforeach;

And after the end of the loop don’t forget to reset the post data:

wp_reset_postdata();

 

Display Custom Tooltips in Chart.js 3

Chart.js is a lovely library for interactive charts. It’s probably the best free and open source library for JavaScript based charts available at the moment.

For a project that creates custom charts based on data collected with our quiz and survey WP plugin we had to create custom tooltips that appear when the mouse hovers any of a linear chart datapoints.

The Problem

The default tooltips in chart.js unfortunately allow only text. For our project we needed to include fully featured HTML code – a pop-up with an image and text had to appear when the mouse is hovered on a data point.

The Solution In Action

Here is what you will achieve after following this kind-of a tutorial:

Info about One

Content, including images can be placed here.

Close

Info about Two

Content, including images can be placed here.

Close

Info about Three

Content, including images can be placed here.

Close

Info about Four

Content, including images can be placed here.

Close

Info about Five

Content, including images can be placed here.

Close

You see how the tooltips are fully featured HTML boxes with text, image(s), and a close button.

The Solution in Code

For this post we just created a hardcoded chart with 5 data points and 5 divs with information about them. It’s of course more likely that you will use some dynamic data from a database, etc. So you will need to loop through the data and generate parts of the HTML and JS code. This however does not change the logic that we will describe here.

First, let’s create some basic HTML. You need a div with information (tooltip contents) for each data point. The important thing here is to place these divs AND your chart canvas in a wrapper with position:relative. Each of the info divs has position:absolute. This allows you to properly display the divs pretty close to each data point.

<style type="text/css">
    .pointinfo-container {
        display:none;
        padding: 7px;
        background: white;
        width: 200px;
        border: 1pt solid black;
        position: absolute;
    }
    </style>

    <div style="position: relative;"><!-- This is the wrapper -->
        <div id="pointInfo1" class="pointinfo-container">
            <h3 class="pointinfo-event-title">Info about One</h3>			  		
            <span ><a href="#" onclick="closePointInfo(1);return false;">Close</a></span>
        </div>
        
        <div id="pointInfo2" class="pointinfo-container">
            <h3 class="pointinfo-event-title">Info about Two</h3>
            <span ><a href="#" onclick="closePointInfo(2);return false;">Close</a></span>
        </div>
        
        <div id="pointInfo3" class="pointinfo-container">
            <h3 class="pointinfo-event-title">Info about Three</h3>
            <span ><a href="#" onclick="closePointInfo(3);return false;">Close</a></span>
        </div>
        
        <div id="pointInfo4" class="pointinfo-container">
            <h3 class="pointinfo-event-title">Info about Four</h3>
            <span ><a href="#" onclick="closePointInfo(4);return false;">Close</a></span>
        </div>
        
        <div id="pointInfo5" class="pointinfo-container">
            <h3 class="pointinfo-event-title">Info about Five</h3>
            <span ><a href="#" onclick="closePointInfo(5);return false;">Close</a></span>
        </div>	
        
        <div><canvas id="myChart" style="display: block; box-sizing: border-box; height: 306px; width: 612px;"></canvas></div>
    </div>

For clarity we have omitted most of the contents of the div.

And now, more important, the JavaScript.

var myChart = null;
    jQuery( document ).ready(function() {
            var ctx = document.getElementById('myChart').getContext('2d');
            myChart = new Chart(ctx, {	
            type: 'line',
            data: {
                labels: ['One', 'Two', 'Three', 'Four', 'Five'],
                datasets: [{
                    label: 'Demo Chary',			
                    data: [22, 33, 17, 67, 8],
                    fill: 'false',
                    borderColor: '#052148',
                    backgroundColor: 'white',			
                }],
            },
            options: {
                aspectRatio: 3,
                responsive: true,
                maintainAspectRatio: true,
                layout: {
                    padding: {
                        left: 30,
                        right: 40,
                        top: 70,
                        bottom: 10
                    }
                },
                legend: {
                    display: false
                },
                plugins: {
                    
                },
                tooltips: {
                    enabled: false
                },
        
                onHover: (e) => {	
                    			
                      var meta = myChart.getDatasetMeta(0);
                      datapoints = [
                      	 {'x' : Math.round(meta.data[0].x), 'y': Math.round(meta.data[0].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[1].x), 'y': Math.round(meta.data[1].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[2].x), 'y': Math.round(meta.data[2].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[3].x), 'y': Math.round(meta.data[3].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[4].x), 'y': Math.round(meta.data[4].y)},
                      ]; 
                        
                        
                       if(typeof(datapoints) === 'undefined') return false;
                    const canvasPosition = Chart.helpers.getRelativePosition(e, myChart);
                        
                   jQuery(datapoints).each(function(i, elt){
                   	   let j = i+1;
                   	  
                    	if(elt.x >= canvasPosition.x - 10 && elt.x <= canvasPosition.x + 10 && elt.y >= canvasPosition.y - 10 && elt.y <= canvasPosition.y + 10) {		            		
                    		jQuery('#pointInfo' + j).css( {position:"absolute", top: canvasPosition.y - 50, left: canvasPosition.x - 50});		            		
                    		jQuery('#pointInfo' + j).show();
                    	}
                    });
        
                },			
            } // end options
        }); // end chart		
       
        // on resize we hide all boxes to avoid messy view
       jQuery( window ).resize(function() {
         jQuery('.pointinfo-container').hide();
        }); // end resize		
        
    }); // end document ready

Most of the standard Chart.js code is pretty clear. You need to focus only on the onHover function. Let’s repeat it here with lots of comments:

onHover: (e) => {	
                    	 // this gets the meta data of the chart to figure out the coordinates of each data point
                      // This part is very important and specific to Chart.js version 3. In version 2 the syntax will not work.		
                      var meta = myChart.getDatasetMeta(0);
                      datapoints = [					  	 	 
                      	{'x' : Math.round(meta.data[0].x), 'y': Math.round(meta.data[0].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[1].x), 'y': Math.round(meta.data[1].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[2].x), 'y': Math.round(meta.data[2].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[3].x), 'y': Math.round(meta.data[3].y)},
                      	 	  	 
                      	 {'x' : Math.round(meta.data[4].x), 'y': Math.round(meta.data[4].y)},
                      ]; 
                        
                        
                       if(typeof(datapoints) === 'undefined') return false;

                           // Once we know the datapoints position, we also need to get the x/y of the canvas
                    const canvasPosition = Chart.helpers.getRelativePosition(e, myChart);
                        
                   jQuery(datapoints).each(function(i, elt){
                   	   let j = i+1;
                                  // Remember, we are hovering the mouse. Don't expect the user to be exact and give some allowance which will display the info when the user is close to any
                        // of the data points. Here we give allowance of 10 pixels around each point.
                    	if(elt.x >= canvasPosition.x - 10 && elt.x <= canvasPosition.x + 10 && elt.y >= canvasPosition.y - 10 && elt.y <= canvasPosition.y + 10) {
		            	// and the stuff here is rather straightforward: set the element position around the datapoint position	
                    		jQuery('#pointInfo' + j).css( {position:"absolute", top: canvasPosition.y - 50, left: canvasPosition.x - 50});	
                                // then show	            		
                    		jQuery('#pointInfo' + j).show();
                    	}
                    });
        
                },

The most important part above is getting the data points coordinates. You need to do this inside the onHover function. In Chart.js version 2 you could do it in a document.ready handler, but this no longer works correctly.

The data points information is retrieved through the getDatasetMeta() method of the library. The argument is the dataset number, staring at 0. (Our chart has only one dataset).

Each data point is in the resulting object property meta.data. It is an array of datapoint objects and you need the properties x and y of each of them.

Finally, you need a function to close the pop-up when the user clicks on the close()

function closePointInfo(cnt) {
        jQuery('#pointInfo' + cnt).hide();
    
        // if all point infos are hidden, make sure the show all mode is show
        let anyOpen = false;
        jQuery('.pointinfo-container').each(function(i, elt){
            if(jQuery(elt).is(":visible")) {
                anyOpen = true;
                return;
            }
        });
        
    }

That’s it. If you have used this somewhere, let us know in the comments!