Categories
Programming

Writing a wordpress read time plugin6 min read

Lately, I have been seeing estimated read times near post titles. It went like “2 min read”, “5 min red” , and as a reader it felt good to know how much time I’am going to waste on some random article ? .

So I have decided pass on the luxury ????  . It must be obvious by now that this blog runs on wordpress. There are many plugins available to add this feature to a wordpress blog, but ???? you know…

How to create a wordpress plugin?

It may seem like an uphill task, but actually you can finish one in a few minutes. To learn more, buy my eBook wordpress plugins for dumm.. ? , ehmmm a little bit of PHP and HTML and you are good to go.

For a detailed and fairly simple documentation go to wordpress.com. Start here – Writing a plugin.

You need to add the necessary header and license information on top of your main PHP page for wordpress to identify it as a plugin.

It would look something like this –

<?php
/*
Plugin Name: Calc read time
Plugin URI:  http://jijnasu.in/wp-content/uploads/2016/10/Calc-read-time.zip
Description: Estimates read time for posts and shows it near post titles.
Version:     1
Author:      Jijnasu
Author URI:  http://jijnasu.in/
License:     GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: wporg
Domain Path: /languages
*/

The read time plugin

Estimating the read time

After time and brain intensive calculations, I have come to the conclusion that an average adult can read about 200-250 words per minute (wpm).

Link to my research material – Average reading speed.

So we get the number of words in an article, divide it by 250 and we have our average reading time. As simple as that.

But that’s in english, in PHP it would look like

$post = "Hi guys, today let's talk about the top ten reasons why jijnasu.in is the best blog in the world";
$post_word_count = str_word_count($post);
$words_read_per_min = 250;
$words_read_per_sec = 250 / 60;
$post_read_time_in_sec = ceil($post_word_count / $words_read_per_sec);

PHP – str_word_count.

WordPress integration

WordPress gives events we can register different functions on, like the events in javascript (in usage).

There are two types of events

  1. add_action ( string $tag, callable $function_to_add, int $priority = 10int $accepted_args = 1 )
  2. add_filter ( string $tag, callable $function_to_add, int $priority = 10int $accepted_args = 1 )

$tag represent the event.

Let’s add a filter

add_filter( 'wp_insert_post_data', 'calc_read_time_calc', 1, 2 );

The function ‘calc_read_time_calc’ would run everytime a post is inserted or updated. The event name being ‘wp_insert_post_data’.

The calc_read_time_calc function
<?php

// function to save word count of posts

function calc_read_time_calc($data, $postarr)
{
  $post_word_count = str_word_count($data['post_content']);

  // update meta data if exists else update
  // meta_key: calc_read_time_word_count
  // meta_value : $post_word_count

  if (!add_post_meta($postarr['ID'], 'calc_read_time_word_count', $post_word_count, true))
  {
    update_post_meta($postarr['ID'], 'calc_read_time_word_count', $post_word_count);
  }

  return $data;
}

On insert or update of posts the word count is calculated and saved as meta data with key calc_read_time_word_count .

Showing it to the reader

We can doing this by adding one more filter

add_filter( 'the_title', 'calc_read_time_add_read_text_in_title', 1, 2 );

With this filter we can update the post title (the_title) before printing it on the screen.

We would use the calc_read_time_add_read_text_in_title ? function, to append read time to the post title.

The calc_read_time_add_read_text_in_title function
<?php

// function to append read time
// to the post title

function calc_read_time_add_read_text_in_title($title, $post_id)
{

  // add read time to only posts
  // and not pages or other stuff
  // also not in admin pages

  if (get_post_type($post_id) != 'post' || is_admin())
  {
    return $title;
  }

  // retrive word count saved
  // as meta data

  $word_count = get_post_meta($post_id, 'calc_read_time_word_count', true);

  // if word count was not set
  // set it for this post

  if (empty($word_count))
  {
    calc_read_time_calc(array(
      'post_content' => get_post_field('post_content', $post_id)
    ) , array(
      'ID' => $post_id
    ));
    $word_count = get_post_meta($post->ID, 'calc_read_time_word_count', true);
  }

  $word_count = (int)$word_count;
  if ($word_count == 0)
  {
    return $title;
  }

  // append word count to the title

  return $title . '<span class="calc_read_time_shower_title_span">' . calc_read_time_create_text($word_count) . '</span>';
}

If word count was not set for some post, it would be set inside this function.

If you paid any attention at all, which is highly unlikely, u must have noticed another function calc_read_time_create_text at the end.

It is used to calculate the reading time with the word count as input.

The calc_read_time_create_text function
<?php

// function to calculate
// read time with word_count

function calc_read_time_create_text($post_word_count)
{

  // words read per minute

  $wpm = 240;

  // words read per second

  $wps = $wpm / 60;
  $secs_to_read = ceil($post_word_count / $wps);
  $read_time_text = $secs_to_read < 60 ? $secs_to_read . ' sec' : round($secs_to_read / 60) . ' min';
  $read_time_text.= ' read';

  // ex - 5min read

  return $read_time_text;
}

Ideally the user should be able to set the words read per minute, but let’s look at it later.

And finally let’s add some CSS.

The style sheet
.calc_read_time_shower_title_span{
  display: block;
  font-size: 15px;
  background: inherit;
  color: inherit;
}

Adding style sheets in wordpress

add_action( 'wp_enqueue_scripts', 'calc_read_time_add_styles' );

Use the add action to add style-sheets or scripts to your pages.

The calc_read_time_add_styles function
function calc_read_time_add_styles()
{
  wp_enqueue_style('calc-read-time-styles', plugin_dir_url(__FILE__) . 'calc-read-time-styles.css', array() , null);
}

Notes

  • The function names must be unique.
  • The style-sheet names must be unique.
  • The plugin folder name should not contain spaces. Replace spaces with hyphens. Ex- ‘calc-read-time’ not ‘Calc read time’.
  • Zip the plugin folder and upload it to your wordpress site.

Download the plugin – calc-read-time.

Download it on wordpress.org – Calc Read Time

Good day ????

Categories
Programming

Creating a Javascript load more plugin part 2 – Searching2 min read

In the first part Creating a Javascript load more plugin part 1 , we have created a javascript plugin which loads data from an API page. Let’s build upon on it.

Today we are going to add search option to our plugin, children ? .

Objectives ?

  • Add search bar to the page.
  • On input on search bar, load in relevant content from the API.
  • The API should take in a search parameter and provide relevant results.

As of now we are sending two parameters to the API – start and count. We would add one more q for sending the search query.

For learning how add searching option at the server end, please refer this tutorial from Codecourse – Basic PHP & MySQL Searching.

It uses PHP and MYSQL but you get the idea ? .

Demo page – Javascript load more plugin – search added.

We have to add one more line to our previous HTML code to include the search bar.

<input type="search" autocomplete="off" class="finder" />

We can add this anywhere inside our main container. Only condition is that it should have a class of finder, so that our plugin can identify it.

For styling HTML forms and form elements view this tutorial – Styling HTML Forms .

Updating the Plugin

Now let’s upgrade our plugin. First we would find the search bar.

//Search bar
this.finder = this.elem.getElementsByClassName('finder').length ? this.elem.getElementsByClassName('finder')[0] : false;

If you are not able to follow, please go through part one of this series.

Now, inside our lmore.load method we would append the search term to the GET query string.

//create query string with
//start and count variables
var qs = this.source + '?lmore_start=' + this.start + '&lmore_count=' + this.count;

//Send search query if search bar is available
qs += this.finder ? '&q=' + this.finder.value.trim() : '';
this.http.open('GET', qs);
this.http.send();

Let’s add and event listener to the search bar so that, data can be loaded while searching.

We could load data on any event, but I prefer the on-input event because it gives an instant search effect.

I don’t like search bars that have a button which has to be clicked to search. We are in 2016, common guys ( who make me click a button to search ? )  ?.

We could even pass the event, as input for the plugin.

if (this.finder) {
  this.finder.addEventListener('input', function() {
    this.load();
  }.bind(this));
}

You could also use this plugin to give search suggestions like google. Maybe we could use the plugin two times, one for suggestions and one for showing search results.

Comment below if you want to see this plugin give you search suggestions.

Categories
Programming

Javascript load more plugin6 min read

When searching the internet for a Javascript load more plugin, I came across a Jquery load more plugin tutorial in this youtube channel – Codecourse.

I highly recommend that the reader watch the Codecourse (formerly PHPacademy) tutorial and subscribe to their channel before continuing.

Link to the tutorial – Load More jQuery Plugin.

The tutorial was a really good and the plugin was very useful. But, with the aim of getting better at javascript and programming in general I tried to customize it.

So I decided to redo it in pure javascript. Here is how it went.

Demo link – Javascript load more plugin demo.

Please refer the above tutorial for creating a server side JSON API. The API would look like this Load more JSON API.

Now let’s get started with our plugin.

How to use?

<script src="lmore.js"</script>
<script>
  var itemload=new lmore({elem:cont,source:'json-api-load-more-plugin/',count:3});
</script>

Parameters

  • elem: The main container.
  • source: JSON API page.
  • count: Number of results to load per request.

HTML

The plugin requires an HTML template to work with and the template would look like this.

<div id="cont">
  <span class="rescont" >
    <div id="ent_cont" class="items" >
      <div class="item" >
        <p data-field="id" class="hide" ></p>
        <h2 data-field="title" ></h2>
        <p data-field="description" ></p>
      </div>
    </div>
  </span>
  <div id="no_result" class="nores" >No results to display.</div>
  <button id="load_more" class="load_more" >Load more</button>
</div>

Elements included are

  • #cont – Main Container.
  • .rescont – Container where results have to be loaded.
  • .items – Container for .item
  • .item – Individual data container which would be cloned with each result set.
  • [data-field] – The value of this attribute would be matched with the keys in the JSON object and corresponding values would be filled.
  • .nores – The element to be shown when there are no results. .rescont would be hidden and .nores would be shown if there are no results.
  • .load_more – Button which on click would load more results.

JAVASCRIPT

The plugin would take 3 inputs as given in the usage section.

It would have two main functions

  1. load – A function to load data from the JSON API page using ajax.
  2. append – A function to append the loaded data to the DOM.

The plugin

Wrapper
var lmore = function(options) {
  this.append = function() {

  }
  this.load = function() {

  }
}
Now we have to set the necessary properties.
//Default options
this.def_option = {
  elem: NaN,
  source: NaN,
  count: 3
};

//set the properties
if (options) {
  for (key in options) {
    if (this.def_option.hasOwnProperty(key)) {
      this[key] = options[key];
    }
  }
}

//Http request variable
this.http = 0;

//Starting point
this.start = 1;

//Load more button
this.load_more = false;

//Whether it load more button was clicked or not
this.is_load_more = false;
Getting load more button and saving it and it’s display property
  //set load more button
  if (this.elem.getElementsByClassName('load_more')[0]) {
    this.load_more = this.elem.getElementsByClassName('load_more')[0];
    this.load_more_display = this.load_more.style.display;
    this.load_more.style.display = 'none';
  }
Cloning .item element and removing it from the DOM.
//Clone .item and remove it from DOM
this.items = this.elem.getElementsByClassName('items')[0];
this.itemo = this.items.getElementsByClassName('item')[0];
this.item = this.itemo.cloneNode(true);
this.items.removeChild(this.itemo);
this.nores = this.elem.getElementsByClassName('nores')[0];
this.rescont = this.elem.getElementsByClassName('rescont')[0];
this.resdis = this.rescont.style.display;

//function to remove .item from DOM
this.remove = function() {
  while (this.items.firstChild) {
    this.items.removeChild(this.items.firstChild);
  }
}
The load function
this.load = function() {
  //Check if load more button was clicked
  if (!this.is_load_more) {
    this.start = 1;
    this.remove();
  } else {
    this.start += this.count;
  }


  this.is_load_more = false;

  //Abort pending ajax calls if any
  if (this.http != 0) {
    this.http.abort();
  }

  //Ajax request
  this.http = new XMLHttpRequest();
  this.http.onreadystatechange = function() {
    if (this.http.readyState == 4 && this.http.status == 200) {
      res = JSON.parse(this.http.responseText);
      resp = res.items;

      //If last result hide load more button else show
      if (this.load_more) {
        if (!res.last) {
          this.load_more.style.display = this.load_more_display;
        } else {
          this.load_more.style.display = 'none';
        }
      }

      //Check if there are results
      //If not the no results text is shown
      if (undefined !== resp && resp != null && resp.length) {
        //Loop through each result set
        //and pass it to append function
        for (var i = 0, q = resp.length; i < q; i++) {
          this.append(resp[i]);
        }
        this.nores.style.display = 'none';
        this.rescont.style.display = this.resdis;
      } else {
        this.nores.style.display = 'block';
        this.rescont.style.display = 'none';
      }
    }
  }.bind(this);
  //.bind is used so that 'this' is set
  //to lmore object

  //create query string with 
  //start and count variables
  var qs = this.source + '?lmore_start=' + this.start + '&lmore_count=' + this.count;
  this.http.open('GET', qs);
  this.http.send();
};
The append function
//Append API data to DOM
this.append = function(value) {

  //Create a clone of the .item 
  var clone = this.item.cloneNode(true);

  //select all elements in .item
  //with data-field attribute
  var subs = clone.querySelectorAll('[data-field]');

  //loop through API result
  for (name in value) {
    if (value.hasOwnProperty(name)) {

      //loop through elements
      //with data-field attr
      for (var i = 0, q = subs.length; i < q; i++) {

        //id attr = key of object
        //set value
        if (subs[i].getAttribute('data-field') == name) {
          var ptext = value[name];
          var text = document.createTextNode(ptext);
          this.set_val(subs[i], ptext);
        }
      }
    }
  }

  //append the cloned element
  //to .items
  this.items.appendChild(clone);
};
Function to set value of any element
//Function to set value of any element
//params el - element to set value to
//val - the value to be set
this.set_val = function(el, val) {
  var node_type = el.nodeName.toLowerCase();
  if (node_type != 'input' && node_type != 'select' && node_type != 'textarea') {
    el.innerHTML = val;
    return true;
  }
  if (node_type == 'select' || (node_type == 'input' && el.type != 'checkbox') || node_type == 'textarea') {
    if (el.hasAttribute('multiple')) {
      this.multi_set(el, val);
      return true;
    }
    el.value = val;
    return true;
  } else if (el.type == 'checkbox') {
    el.checked = (val == '1') ? true : false;
    return true;
  }
  return false;
}

//Function to set value of select multiple
this.multi_set = function(sel, values) {
  var vals = Array.isArray(values) ? values : String(values).split(',');
  var opt;
  for (var i = 0, len = sel.options.length; i < len; i++) {
    opt = sel.options[i];
    opt.selected = false;
    for (var x = 0, q = vals.length; x < q; x++) {
      if (opt.value == vals[x]) {
        opt.selected = true;
      }
    }
  }
}
Initial loading

Also setting event listener on load more button to load data on click

//Load first set of data on page load
this.load('');

//The load more button on click would load
//the nextr set of results
if (this.load_more) {
  this.load_more.addEventListener('click', function() {

    //is_load_more is set to true
    //so that start is not reset
    this.is_load_more = true;
    this.load();
  }.bind(this));
}

And finally the plugin is complete ????.

Download link – lmore.js.

Leave your suggestions below.