Calendar of CPT Using ACF Date/Time Field

This is a work in progress. Displays a small calendar with dates specified using a date/time field in Advanced Custom Fields on a Custom Post Type. Currently assumes a posts to posts connection between two CPTs projects and activities.
Screen Shot 2013-12-23 at 0.26.47

The PHP

jlv-cpt-calendar.php

<?php
/*
Plugin Name: JLV CPT Calendar
Plugin URI: http://dev.abuyasmeen.com/
Description: CPT Calendar with ACF
Version: 0.3
Author: Jeremy Varnham
Author URI: https://abuyasmeen.com/
*/

define( 'JLV_CPT_CAL_VERSION', '3' );
define( 'JLV_CPT_CAL_URL', plugin_dir_url( __FILE__ ) );
define( 'JLV_CPT_CAL_DIR', dirname( __FILE__ ) . '/' );

require JLV_CPT_CAL_DIR . 'jlv-cpt-calendar-scriptinit.php';
require JLV_CPT_CAL_DIR . 'jlv-cpt-calendar-loader.php';
require JLV_CPT_CAL_DIR . 'jlv-cpt-calendar-widget.php';

?>

jlv-cpt-calendar-loader.php

<?php
/*
*/

class jlv_cpt_calendar {

	/**
	 * Define static values for testing purposes.
	 * Will be replaced with WordPress options and user input.
	 */
	public $strPostType = 'project';
	public $strStartTimeACF = 'start_time';

	public $intSelectedYear = 0;
	public $intSelectedMonth = 0;

	public $intFirstDayOfWeek = 0;

	/**
	 * Define blank arays to be built up with functions below.
	 */
    private $_WeekDays = array();
	private $_Structure = array();
	private $_TheDays = array();

	/**
	 * Defines the days of the week starting with Sunday in English and Arabic.
	 *
	 * @since 0.2
	 */
	private function define_week_days() { 
		$this->_WeekDays['en-US'] = array('Sun',  'Mon',  'Tue',  'Wed',  'Thur',  'Fri',  'Sat');
		$this->_WeekDays['ar'] = array('الأحد',  'الاثنين',  'الثلاثاء',  'الأربعاء',  'الخميس',  'الجمعة',  'السبت');
	}

	/**
	 * Extracts various numbers and strings from the given date.
	 *
	 * @since 0.1
	 */
	private function define_day_codes() { 
		$firstDayOfMonthTimeCode							= (int) mktime(0, 0, 0, $this->intSelectedMonth, 1, $this->intSelectedYear);
		$this->_TheDays['Total Days In Month']				= (int) date('t', $firstDayOfMonthTimeCode);
		$this->_TheDays['Weekday For First Day Of Month']	= (int) date('w', $firstDayOfMonthTimeCode); 
		$this->_TheDays['The Month Name']					= (string) date('F', $firstDayOfMonthTimeCode);
		$this->_TheDays['Current Date Int']					= (int) date("Ymd",mktime(0,0,0,date("m"),date("d"),date("Y")));  
	}

	/**
	 * Defines the HTML parts in one place for more readable output function.
	 *
	 * @since 0.2
	 */
    private function define_html_structure() {
    	$this->_Structure['Start Cal Div']			= '<div class="cal">';
    	$this->_Structure['End Cal Div']			= '</div>';
		$this->_Structure['Start Table']			= '<table class="cal-table" style="background: none;">';
		$this->_Structure['End Table']				= '</table>';
		$this->_Structure['Start Table Header']		= '<thead><tr>';
		$this->_Structure['End Table Header']		= '</tr></thead>';
		$this->_Structure['Start Table Caption']	= '<caption class="cal-caption">';
		$this->_Structure['End Table Caption']		= '</caption>';
		$this->_Structure['Start Table Body']		= '<tbody class="cal-body"><tr>';
		$this->_Structure['End Table Body']			= '</tbody>';
		$this->_Structure['Start Table Column']		= '<td>';
		$this->_Structure['End Table Column']		= '</td>';
		$this->_Structure['End Table Row']			= '</tr>';
    }

	/**
	* Converts slashes to dashes for compatibility with already entered dates  
	*
	* @since 0.2
	*
	* @param string $the_date the date saved by ACF date/time picker
	* @return int $the_date_int the date as Ymd
	*/
	private function convert_the_date($the_date) {

		if (strpos($the_date,'/') !== false) { // if it's the old format (dd/mm/yy)
			$the_date_bits = explode('/',$the_date);
			$the_date = $the_date_bits[0].'-'.$the_date_bits[1].'-'.$the_date_bits[2]; 
			$the_date_int = (int) date('Ymd', strtotime($the_date)); // Strip time, strip dashes, make integer
		}
		else
		{
			$the_date_int = (int) date('Ymd', strtotime($the_date)); // Strip time, strip dashes, make integer
		}
		return $the_date_int;
	}

	/**
	 * Performs the WordPress query.
	 *
	 * @since 0.1
	 *
	 * @return object
	 */
	private function do_the_query() {
		wp_reset_postdata(); 
		global $post;
			$args = array( 
				'post_type' => $this->strPostType, 
				'meta_key' => $this->strStartTimeACF,
				'orderby' => 'meta_value',
				'order' => 'ASC',			
				'connected_type' => 'activity_to_project',
				'connected_items'=>$post->ID
				);
			$loop = new WP_Query( $args );
		return $loop;
	}

	/**
	 * Loops through days of week starting with defined starting day.
	 * Wraps each day name in a TH
	 *
	 * @since 0.1
	 *
	 * @return string $html
	 */
    private function get_the_table_header() {
    	$html = '';
		for ($counter = 0, $i = $this->intFirstDayOfWeek; 7 > $counter; $counter++, $i++) // Loop 7x to output weekday names
			{
				$html .= '<th>' . $this->_WeekDays[get_bloginfo('language')][$i] . '</th>';
		        if (6 == $i) { $i = -1; } // If counter reached to 6, set it to -1
			}
		return $html;
	}

	/**
	 * Builds the table caption with pagination / month name and year.
	 *
	 * @since 0.1
	 *
	 * @return string $html
	 */
	private function get_the_table_caption() {
		$html = '';
    	//$html .= '<a href="#" class="prev">&laquo;</a>';
		//$html .= '<a href="#" class="next">&raquo;</a>';
		$html .= $this->_TheDays['The Month Name'] . ' ' . $this->intSelectedYear; 
		return $html;
	}

	/**
	 * If new week has started then close current table row and start new one
	 *
	 * @since 0.1
	 *
	 * @return string $html
	 */
	private function start_new_row($day) {
		$html = '';
    	$current_day_of_week = date('w', mktime(0, 0, 0, $this->intSelectedMonth, $day, $this->intSelectedYear));								        if (1 < $day && $this->intFirstDayOfWeek == $current_day_of_week )
        {
        	$html .= '</tr><tr>';
        } 
        return $html;
     }

	/**
     * Checks if 'first day of week' is not equal to weekday for first day of month 
     * then outputs empty TDs
	 *
	 * @since 0.1
	 *
	 * @return string $html
     */
	private function get_the_empty_days_before() {
		$html = '';
	    if ($this->intFirstDayOfWeek != $this->_TheDays['Weekday For First Day Of Month'])
	    {
	        $totalEmptyDays = ($this->_TheDays['Weekday For First Day Of Month'] - $this->intFirstDayOfWeek); // Calculate total empty days
	        if ($this->intFirstDayOfWeek > $this->_TheDays['Weekday For First Day Of Month']) // If 1st day of week > weekday for 1st day of month, +7 days to empty days
	        {
	            $totalEmptyDays += 7;
	        }

	        for ($i = 0; $i < $totalEmptyDays; $i++) // Empty TDs if 1st day of month doesn't start on '1st day of week'
	        {
	        	$html .= '<td class="cal-off">&nbsp;</td>';
	        }
	    }
	    return $html;
    }

	/**
     * Checks if 'last day of week' is not equal to weekday for last day of month 
     * then outputs empty TDs
	 *
	 * @since 0.1
	 *
	 * @return string $html
     */
    private function get_the_empty_days_after() {
	    $weekdayForLastDayOfMonth = date('w', mktime(0, 0, 0, $this->intSelectedMonth, $this->_TheDays['Total Days In Month'], $this->intSelectedYear));
	    $totalEmptyDays = ($this->intFirstDayOfWeek - $weekdayForLastDayOfMonth - 1); // Calculate total empty days

	    if ($this->intFirstDayOfWeek <= $weekdayForLastDayOfMonth)
	    {
	        $totalEmptyDays += 7;
	    }
		$html = '';
	    for ($i = 0; $i < $totalEmptyDays; $i++)
	    {

	    $html .= '<td class="cal-off">&nbsp;</td>';
	    }
		return $html;
	}

	/**
	 * Prints the day numbers into each table cell
	 *
	 * @since 0.1
	 *
	 * @return string $html
	 */
	private function write_day_numbers($day) {
		$html = '';
			$html .= '<a class="day">' . $day . '</a>';
        return $html;
     }

	/**
	 * If any post(s) for current day in current month/year then display them
	 *
	 * @since 0.1
	 *
	 * @return string $html
	 */
	private function get_day_activities($loop, $day) {
		$html = '';
		$numberofcolumns = 1; $count = $numberofcolumns;

		while ($loop->have_posts()) : $loop->the_post(); 
			$start_date = get_field($this->strStartTimeACF);
			$start_date_int = $this->convert_the_date($start_date);
			$current_date_int = $this->_TheDays['Current Date Int'];  
			$cal_date = $this->intSelectedYear.'-'.$this->intSelectedMonth.'-'.$day; 
			$cal_date_int = $this->convert_the_date($cal_date); 

			if ($cal_date_int == $start_date_int) 
			{ 
				$html .= '<a href="' . get_permalink() . '" class="hasevent">';
				$html .= '<p style="padding: 0; margin: 0; ">x</p>';
                $html .= '</a>';
            }
			elseif ($cal_date_int == $current_date_int) { 
				$html .= '<a name="istoday" class="istoday"></a>';
			}
		endwhile;
		return $html;
	}

	/**
	 * Outputs HTML before the table.
	 */
	private function do_before_the_table() {
		$html = $this->_Structure['Start Cal Div'];
		$html .= $this->_Structure['Start Table'];
		return $html;
	}

	/**
	 * Outputs the Table Header.
	 */
	private function do_the_table_header() {
		$html = $this->_Structure['Start Table Header'];
		$html .= $this->get_the_table_header(); 
		$html .= $this->_Structure['End Table Header'];
		return $html;
	}

	/**
	 * Outputs the Table Caption.
	 */
	private function do_the_table_caption() {
		$html = $this->_Structure['Start Table Caption'];
		$html .= $this->get_the_table_caption();
		$html .= $this->_Structure['End Table Caption'];
		return $html;
	}

	/**
	 * Outputs the Table Body.
	 */
	private function do_the_table_body($loop) {
		$html = $this->_Structure['Start Table Body'];
		$html .= $this->get_the_empty_days_before();
	    for ($day = 1; $day <= $this->_TheDays['Total Days In Month']; $day++)
	    {
		    $html .= $this->start_new_row($day);
			$html .= $this->_Structure['Start Table Column'];
			$html .= $this->write_day_numbers($day);
			$html .= $this->get_day_activities($loop,$day);
			$html .= $this->_Structure['End Table Column'];
	    }
		$html .= $this->get_the_empty_days_after();
		return $html;
	}

	/**
	 * Outputs HTML after the table.
	 */
	private function do_after_the_table() {
		$html = $this->_Structure['End Table Row'];
		$html .= $this->_Structure['End Table Body'];
		$html .= $this->_Structure['End Table'];
		$html .= $this->_Structure['End Cal Div'];
		return $html;
	}

	/**
	 * Outputs the HTML.
	 */
	private function make_the_output() {
		$this->define_week_days();
		$this->define_day_codes();
		$this->define_html_structure();

		$loop = $this->do_the_query();
		if ($loop->have_posts()) {
			$html = $this->do_before_the_table();
			$html .= $this->do_the_table_header();
			$html .= $this->do_the_table_caption();
			$html .= $this->do_the_table_body($loop);
			$html .= $this->do_after_the_table();	
		}
		return $html;	
	} 

	public function set_the_date($month,$year) {
		$this->intSelectedYear = (int) $year;
		$this->intSelectedMonth = (int) $month;	
	}

	public function printcalendar($month,$year) {
		$this->intSelectedYear = (int) $year;
		$this->intSelectedMonth = (int) $month;
		echo $this->make_the_output();		
	}

} // end class

?>

 jlv-cpt-calendar-scriptinit.php

<?php
/*
*/

class styleloader {
	/**
	 * Register and enqueue style sheet.
	 *
	 * @since 0.1
	 */
	public function register_plugin_styles() {
	    wp_register_style( 'jlv_cpt_calendar_style', JLV_CPT_CAL_URL . 'jlv-cpt-calendar.css' ); 
		wp_enqueue_style( 'jlv_cpt_calendar_style' );
	}

	/**
	 * Register and enqueue scripts.
	 *
	 * @since 0.1
	 */
	public function register_plugin_scripts() {
		wp_register_script( 'jlv_cpt_calendar_script', JLV_CPT_CAL_URL . 'jlv-cpt-calendar.js', array( 'jquery' ) );
		wp_enqueue_script( 'jlv_cpt_calendar_script' );

	}

	/**
	 * Add style and script actions.
	 *
	 * @since 0.1
	 */
	private function add_plugin_actions() {
		add_action( 'wp_enqueue_scripts', array($this, 'register_plugin_styles') );
		add_action( 'wp_enqueue_scripts', array($this, 'register_plugin_scripts') );
	}

}

$loadstyles = new styleloader();
$loadstyles->register_plugin_styles();
$loadstyles->register_plugin_scripts();

?>

jlv-cpt-calendar-widget.php

<?php
/*
* @since 0.3
*/

class jlv_cpt_acf_cal_widget extends WP_Widget {

	// constructor
	function jlv_cpt_acf_cal_widget() {
		parent::WP_Widget(false, $name = __('Activity Calendar', 'jlv_cpt_acf_widget') );
	}

	// widget form creation
	function form($instance) {

		// Check values
		if ( $instance) {
			$year = (int) esc_attr($instance['year']);
		} else {
			$year = 2013;
		}
		if ( $instance) {
			$month = (int) esc_attr($instance['month']);
		} else {
			$month = 1;
		}
			?>

			<p>
			<label for="<?php echo $this->get_field_id('year'); ?>"><?php _e('Year:', 'jlv_cpt_acf_widget'); ?></label>
			<input class="widefat" id="<?php echo $this->get_field_id('year'); ?>" name="<?php echo $this->get_field_name('year'); ?>" type="text" value="<?php echo $year; ?>" />

			<label for="<?php echo $this->get_field_id('month'); ?>"><?php _e('Month', 'jlv_cpt_acf_widget'); ?></label>
			<select name="<?php echo $this->get_field_name('month'); ?>" id="<?php echo $this->get_field_id('month'); ?>" class="widefat">

			<?php

			$options = array('Jan' => 1, 'Feb'=> 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5, 'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12);		
			foreach ($options as $key => $value) {
				echo '<option value="' . $value . '" id="' . $key . '"', $month == $value ? ' selected="selected"' : '', '>', $key, '</option>';
			}

			?>

			</select>

			</p>

			<?php
		}

	// update widget
	function update($new_instance, $old_instance) {
		$instance = $old_instance;
		// Fields
		$instance['year'] = strip_tags($new_instance['year']);
		$instance['month'] = strip_tags($new_instance['month']);
		return $instance;
	}

	// display widget
	function widget($args, $instance) {

		//include_once('google-apps-link.php');

		extract( $args );
		// these are the widget options
		$year = $instance['year'];
		$month = $instance['month'];

		echo $before_widget;
		// Display the widget
		$jlv_cpt_calendar = new jlv_cpt_calendar();

				$jlv_cpt_calendar->printcalendar($month,$year); 

		echo $after_widget;
	}

}

// register widget
add_action('widgets_init', create_function('', 'return register_widget("jlv_cpt_acf_cal_widget");'));

?>

The JS

jQuery(document).ready(function(){
  jQuery("a.hasevent").parent().addClass("cal-check");
  jQuery("a.hasevent").parent().children(':first-child').addClass("haschildevent");
  jQuery("a.istoday").parent().addClass("cal-today");
  jQuery('td.cal-check').click(function(){
  	var thisHref = jQuery(this).find('a.hasevent').attr('href');
  	location.href = thisHref;
  });
});

 The CSS

.cal div, .cal span, .cal p, .cal a, .cal table, .cal tbody, .cal tfoot, .cal thead, .cal tr, .cal th, .cal td 
{
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
  line-height: 1;
  font: 13px/20px 'Lucida Grande', Tahoma, Verdana, sans-serif;
}

.cal table {
  border-collapse: collapse;
  border-spacing: 0;
}

.cal {
  position: relative;
  padding: 4px;
  font-weight: bold;
  background: #bebfc0;
  background: rgba(0, 0, 0, 0.1);
  border-radius: 5px;
  display: inline-block;
  vertical-align: baseline;
  zoom: 1;
  *display: inline;
  *vertical-align: auto;
  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2), 0 1px rgba(255, 255, 255, 0.4);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2), 0 1px rgba(255, 255, 255, 0.4);
}
.cal:before {
  content: '';
  position: absolute;
  bottom: 3px;
  left: 4px;
  right: 4px;
  height: 6px;
  background: #d9d9d9;
  border: 1px solid #909090;
  border-radius: 4px;
  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.cal a {
  text-decoration: none;
}
.cal tr:first-child td {
  border-top: 0;
}
.cal td:first-child {
  border-left: 0;
}
.cal tr:first-child a {
  border-top: 0;
  margin-top: 0;
}
.cal tr:last-child a {
  border-bottom: 0;
  margin-bottom: 0;
}
.cal td:first-child a {
  border-left: 0;
  margin-left: 0;
}
.cal td:last-child a {
  border-right: 0;
  margin-right: 0;
}
.cal tr:last-child td:first-child a {
  border-radius: 0 0 0 3px;
}
.cal tr:last-child td:last-child a {
  border-radius: 0 0 3px 0;
}

.cal-table {
  position: relative;
  margin: 0 0 1px;
  border-collapse: separate;
  border-left: 1px solid #979797;
  border-right: 1px solid #979797;
  border-bottom: 1px solid #bbb;
  border-radius: 0 0 3px 3px;
  -webkit-box-shadow: 1px 0 rgba(0, 0, 0, 0.1), -1px 0 rgba(0, 0, 0, 0.1);
  box-shadow: 1px 0 rgba(0, 0, 0, 0.1), -1px 0 rgba(0, 0, 0, 0.1);
}

.cal-caption {
  width: 100%;
  padding-bottom: 1px;
  line-height: 32px;
  color: white;
  text-align: center;
  text-shadow: 0 -1px rgba(0, 0, 0, 0.3);
  background: #155382;
  border-radius: 3px 3px 0 0;
  background-image: -webkit-linear-gradient(top, #358ab4, #155382 75%, #0e4273);
  background-image: -moz-linear-gradient(top, #358ab4, #155382 75%, #0e4273);
  background-image: -o-linear-gradient(top, #358ab4, #155382 75%, #0e4273);
  background-image: linear-gradient(to bottom, #358ab4, #155382 75%, #0e4273);
  -webkit-box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2), inset 0 1px rgba(0, 0, 0, 0.1), inset 0 2px rgba(255, 255, 255, 0.25), 0 1px 3px rgba(0, 0, 0, 0.4);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2), inset 0 1px rgba(0, 0, 0, 0.1), inset 0 2px rgba(255, 255, 255, 0.25), 0 1px 3px rgba(0, 0, 0, 0.4);
}
.cal-caption a {
  line-height: 30px;
  padding: 0 10px;
  font-size: 20px;
  font-weight: normal;
  color: white;
}
.cal-caption .prev {
  float: left;
}
.cal-caption .next {
  float: right;
}

.cal-body td {
  width: 45px;
  font-size: 11px;
  border-top: 1px solid #eaeaea;
  border-left: 1px solid #eaeaea;
}
.cal-body a.day {
  display: block;
  position: relative;
  line-height: 28px;
  color: #555;
  text-align: center;
  background: white; cursor: default;
}
.cal-body a:hover {
  background: #fafafa;
}
.cal-body a.hasevent {
	float: left; 
	position: relative;
	display: none;
}

.cal-off {
	background-color: #eee;
}
.cal-off a {
  color: #ccc;
  font-weight: normal; 
}

.cal-today a.day {
  color: black;
  background: #f5f5f5;
  background-image: -webkit-linear-gradient(top, whitesmoke, white 70%);
  background-image: -moz-linear-gradient(top, whitesmoke, white 70%);
  background-image: -o-linear-gradient(top, whitesmoke, white 70%);
  background-image: linear-gradient(to bottom, whitesmoke, white 70%); 
}

.cal-selected a, .cal-body a:active {
  margin: -1px;
  color: #b2494d;
  background: #fff5f6;
  border: 1px solid #e7d4d4;
}

.cal-check a.event, .haschildevent {
  color: #f79901;
  overflow: hidden; cursor: pointer!important; cursor: hand!important;
}

.cal-check a.event:before, .haschildevent:before {
  content: '';
  position: absolute;
  top: -6px;
  right: -6px;
  width: 12px;
  height: 12px;
  background: #ffb83b;
  background-image: -webkit-linear-gradient(top, #ffb83b, #ff6c00);
  background-image: -moz-linear-gradient(top, #ffb83b, #ff6c00);
  background-image: -o-linear-gradient(top, #ffb83b, #ff6c00);
  background-image: linear-gradient(to bottom, #ffb83b, #ff6c00);
  -webkit-transform: rotate(-45deg);
  -moz-transform: rotate(-45deg);
  -ms-transform: rotate(-45deg);
  -o-transform: rotate(-45deg);
  transform: rotate(-45deg);
}

.lt-ie8 .cal-table {
  *border-collapse: collapse;
}
.lt-ie8 .cal-body a {
  zoom: 1;
}

The CSS is adapted from Mini Calendar

Display the calendar by placing the widget in a sidebar, where you can select the month and year, or directly in a template using:

<?php $jlv_cpt_calendar->printcalendar(12,2013); ?>

where 12 is the month and 2013 is the year

Download Source

1 Comment

Add Yours →

For anyone still looking for a soluion, i have created a calendar plugin that works with custom post type and uses ACF custom_date to display the articles. If you need it, email me at : vivi@spraystudio.ro, and i can give it to you.

Right now i am trying to clean it a bit, and will be uploading it to wordpress repository asap.

Leave a Reply