Source: my-calendar-widgets.php

<?php
/**
 * Construct widgets. Incorporate widget classes & supporting widget functions.
 *
 * @category Calendar
 * @package  My Calendar
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-calendar/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require __DIR__ . '/includes/widgets/class-my-calendar-simple-search.php';
require __DIR__ . '/includes/widgets/class-my-calendar-filters.php';
require __DIR__ . '/includes/widgets/class-my-calendar-today-widget.php';
require __DIR__ . '/includes/widgets/class-my-calendar-upcoming-widget.php';
require __DIR__ . '/includes/widgets/class-my-calendar-mini-widget.php';

/**
 * Generate the widget output for upcoming events.
 *
 * @param array $args Event selection arguments.
 *
 * @return String HTML output list.
 */
function my_calendar_upcoming_events( $args ) {
	$language = isset( $args['language'] ) ? $args['language'] : '';
	$switched = '';
	if ( $language ) {
		$locale   = get_locale();
		$switched = mc_switch_language( $locale, $language );
	}
	$after      = ( isset( $args['after'] ) ) ? $args['after'] : 'default';
	$type       = ( isset( $args['type'] ) ) ? $args['type'] : 'default';
	$category   = ( isset( $args['category'] ) ) ? $args['category'] : 'default';
	$substitute = ( isset( $args['fallback'] ) ) ? $args['fallback'] : '';
	$order      = ( isset( $args['order'] ) ) ? $args['order'] : 'asc';
	$skip       = ( isset( $args['skip'] ) ) ? $args['skip'] : 0;
	$author     = ( isset( $args['author'] ) ) ? $args['author'] : 'default';
	$host       = ( isset( $args['host'] ) ) ? $args['host'] : 'default';
	$ltype      = ( isset( $args['ltype'] ) ) ? $args['ltype'] : '';
	$lvalue     = ( isset( $args['lvalue'] ) ) ? $args['lvalue'] : '';
	$from       = ( isset( $args['from'] ) ) ? $args['from'] : '';
	$to         = ( isset( $args['to'] ) ) ? $args['to'] : '';
	$site       = ( isset( $args['site'] ) ) ? $args['site'] : false;
	$time       = ( isset( $args['time'] ) ) ? $args['time'] : '';

	if ( $site ) {
		$site = ( 'global' === $site ) ? BLOG_ID_CURRENT_SITE : $site;
		switch_to_blog( $site );
	}

	$hash         = md5( implode( ',', $args ) );
	$output       = '';
	$defaults     = mc_widget_defaults();
	$display_type = ( 'default' === $type ) ? $defaults['upcoming']['type'] : $type;
	$display_type = ( '' === $display_type ) ? 'events' : $display_type;

	// Get number of units we should go into the future.
	$args['after'] = ( 'default' === $args['after'] ) ? $defaults['upcoming']['after'] : $args['after'];
	$args['after'] = ( '' === $args['after'] ) ? 10 : $args['after'];

	// Get number of units we should go into the past.
	$args['before'] = ( 'default' === $args['before'] ) ? $defaults['upcoming']['before'] : $args['before'];
	$args['before'] = ( '' === $args['before'] ) ? 0 : $args['before'];
	$category       = ( 'default' === $category ) ? '' : $category;

	/**
	 * Pass a custom template to the upcoming events list. Template can either be a template key referencing a stored template or a template pattern using {} template tags.
	 *
	 * @hook mc_upcoming_events_template
	 *
	 * @param {string} $template Un-parsed template.
	 *
	 * @return {string} Template string.
	 */
	$args['template'] = apply_filters( 'mc_upcoming_events_template', $args['template'] );
	$default          = ( ! $args['template'] || 'default' === $args['template'] ) ? $defaults['upcoming']['template'] : $args['template'];
	$args['template'] = mc_setup_template( $args['template'], $default );

	$no_event_text  = ( ! $substitute ) ? $defaults['upcoming']['text'] : $substitute;
	$lang           = ( $switched ) ? ' lang="' . esc_attr( $switched ) . '"' : '';
	$class          = ( 'card' === $args['template'] ) ? 'my-calendar-cards' : 'list-events';
	$header         = "<div class='mc-event-list-container'><ul id='upcoming-events-$hash' class='mc-event-list upcoming-events $class'$lang>";
	$footer         = '</ul></div>';
	$display_events = ( 'events' === $display_type || 'event' === $display_type ) ? true : false;
	if ( ! $display_events ) {
		$temp_array = array();
		if ( ! empty( $args['from'] ) && ! empty( $args['to'] ) ) {
			$from = $args['from'];
			$to   = $args['to'];
		} else {
			$args = mc_set_from_and_to( $args, $display_type );
			$from = $args['from'];
			$to   = $args['to'];
		}
		/**
		 * Custom upcoming events date start value for upcoming events lists using date parameters.
		 *
		 * @hook mc_upcoming_date_from
		 *
		 * @param {string} $from Starting date for this list of upcoming events in Y-m-d format.
		 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
		 *
		 * @return {string} List starting date.
		 */
		$from = apply_filters( 'mc_upcoming_date_from', $from, $args );
		/**
		 * Custom upcoming events date end value for upcoming events lists using date parameters.
		 *
		 * @hook mc_upcoming_date_to
		 *
		 * @param {string} $to Ending date for this list of upcoming events in Y-m-d format.
		 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
		 *
		 * @return {string} List ending date.
		 */
		$to = apply_filters( 'mc_upcoming_date_to', $to, $args );

		$query = array(
			'from'     => $from,
			'to'       => $to,
			'category' => $category,
			'ltype'    => $ltype,
			'lvalue'   => $lvalue,
			'author'   => $author,
			'host'     => $host,
			'search'   => '',
			'source'   => 'upcoming',
			'site'     => $site,
		);
		/**
		 * Modify the arguments used to generate upcoming events.
		 *
		 * @hook mc_upcoming_attributes
		 *
		 * @param {array} $query All arguments used to generate this list.
		 * @param {array} $args Subset of parameters used to generate this list's ID hash.
		 *
		 * @return {array} Array of event listing arguments.
		 */
		$query       = apply_filters( 'mc_upcoming_attributes', $query, $args );
		$event_array = my_calendar_events( $query );

		if ( 0 !== count( $event_array ) ) {
			foreach ( $event_array as $key => $value ) {
				if ( is_array( $value ) ) {
					foreach ( $value as $k => $v ) {
						if ( mc_private_event( $v ) ) {
							// this event is private.
						} else {
							$temp_array[] = $v;
						}
					}
				}
			}
		}
		$i         = 0;
		$last_item = '';
		$skips     = array();
		$omit      = array();
		foreach ( reverse_array( $temp_array, true, $order ) as $event ) {
			$details = mc_create_tags( $event );
			$data    = array(
				'event'    => $event,
				'tags'     => $details,
				'template' => $args['template'],
				'args'     => $args,
				'class'    => ( str_contains( $args['template'], 'list_preset_' ) ) ? "list-preset $args[template]" : '',
			);
			if ( 'card' === $args['template'] ) {
				$item = '<li class="card-event"><h3>' . mc_load_template( 'event/card-title', $data ) . '</h3>' . mc_load_template( 'event/card', $data ) . '</li>';
			} else {
				$item = mc_load_template( 'event/upcoming', $data );
			}
			/**
			 * Draw a custom template for upcoming events. Returning any non-empty string short circuits other template settings.
			 *
			 * @hook mc_draw_upcoming_event
			 *
			 * @param {string} $item Empty string before event template is drawn.
			 * @param {array}  $details Associative array of event template tags.
			 * @param {string} $template Template string passed from widget or shortcode.
			 * @param {array}  $args Associative array holding the arguments used to generate this list of upcoming events.
			 *
			 * @return {string} Event template details.
			 */
			$item = apply_filters( 'mc_draw_upcoming_event', $item, $details, $args['template'], $args );
			// if an event is a multidate group, only display first found.
			if ( in_array( $event->event_group_id, $omit, true ) ) {
				continue;
			}
			if ( '1' === $event->event_span ) {
				$omit[] = $event->event_group_id;
			}
			if ( '' === $item ) {
				$item = mc_format_upcoming_event( $data, $args['template'], 'list' );
			}
			if ( $i < $skip && 0 !== $skip ) {
				++$i;
			} else {
				// Recurring events should only appear once.
				if ( ! in_array( $details['dateid'], $skips, true ) ) {
					$output .= ( $item === $last_item ) ? '' : $item;
				}
			}
			$skips[]   = $details['dateid']; // Prevent the same event from showing more than once.
			$last_item = $item;
		}
	} else {
		$query  = array(
			'category' => $category,
			'before'   => $args['before'],
			'after'    => $after,
			'author'   => $author,
			'host'     => $host,
			'ltype'    => $ltype,
			'lvalue'   => $lvalue,
			'site'     => $site,
			'time'     => $time,
		);
		$events = mc_get_all_events( $query );

		$holidays      = mc_get_all_holidays( $args['before'], $args['after'], $time );
		$holiday_array = mc_set_date_array( $holidays );

		if ( is_array( $events ) && ! empty( $events ) ) {
			$event_array = mc_set_date_array( $events );
			if ( is_array( $holidays ) && count( $holidays ) > 0 ) {
				$event_array = mc_holiday_limit( $event_array, $holiday_array ); // if there are holidays, filter results.
			}
		}
		if ( ! empty( $event_array ) ) {
			$args['time'] = $time;
			$output      .= mc_produce_upcoming_events( $event_array, $args, 'list' );
		} else {
			$output = '';
		}
	}
	/**
	 * Replace the list header for upcoming events lists. Default value `<ul id='upcoming-events-$hash' class='mc-event-list upcoming-events'$lang>`.
	 *
	 * @hook mc_upcoming_events_header
	 *
	 * @param {string} $header Existing upcoming events header HTML.
	 *
	 * @return {string} List header HTML.
	 */
	$header = apply_filters( 'mc_upcoming_events_header', $header );
	/**
	 * Replace the list footer for upcoming events lists. Default value `</ul>`.
	 *
	 * @hook mc_upcoming_events_footer
	 *
	 * @param {string} $header Existing upcoming events footer HTML.
	 *
	 * @return {string} List header HTML.
	 */
	$footer = apply_filters( 'mc_upcoming_events_footer', $footer );
	if ( '' !== $output ) {
		$navigation = ( 'days' === $args['type'] ) ? mc_upcoming_dates_navigation( $args ) : '';
		$output     = $header . $navigation . $output . $footer;
		$return     = mc_run_shortcodes( $output );
	} else {
		$navigation = ( 'days' === $args['type'] ) ? mc_upcoming_dates_navigation( $args ) : '';
		$header     = str_replace( 'mc-event-list ', 'mc-event-list no-events-fallback ', $header );
		$class      = ( str_contains( $args['template'], 'list_preset_' ) ) ? "list-preset $args[template]" : '';
		$return     = $header . $navigation . '<li class="' . $class . '">' . wp_unslash( $no_event_text ) . '</li>' . $footer;
	}

	if ( $site ) {
		restore_current_blog();
	}

	if ( $language ) {
		mc_switch_language( $language, $locale );
	}

	return $return;
}

/**
 * Generate from and to values from arguments.
 *
 * @param array  $args Upcoming Event arguments.
 * @param string $display_type Type of display.
 *
 * @return array
 */
function mc_set_from_and_to( $args, $display_type ) {
	if ( 'days' === $display_type ) {
		$args['from'] = mc_date( 'Y-m-d', strtotime( "-$args[before] days" ), false );
		$args['to']   = mc_date( 'Y-m-d', strtotime( "+$args[after] days" ), false );
	}

	if ( 'month' === $display_type ) {
		$args['from'] = mc_date( 'Y-m-1' );
		$args['to']   = mc_date( 'Y-m-t' );
	}

	if ( 'custom' === $display_type && '' !== $args['from'] && '' !== $args['to'] ) {
		$args['from'] = mc_date( 'Y-m-d', strtotime( $args['from'] ), false );
		$args['to']   = ( 'today' === $args['to'] ) ? current_time( 'Y-m-d' ) : mc_date( 'Y-m-d', strtotime( $args['to'] ), false );
	}

	for ( $i = 1; $i <= 12; ++$i ) {
		if ( 'month+' . $i === $display_type ) {
			$args['from'] = mc_date( 'Y-m-1', strtotime( '+' . $i . ' month' ), false );
			$args['to']   = mc_date( 'Y-m-t', strtotime( '+' . $i . ' month' ), false );
		}
	}

	if ( 'year' === $display_type ) {
		$args['from'] = mc_date( 'Y-1-1' );
		$args['to']   = mc_date( 'Y-12-31' );
	}

	return $args;
}
/**
 * For a set of grouped events, get the total time spanned by the group of events.
 *
 * @param int $group_id Event Group ID.
 *
 * @return array beginning and ending dates
 */
function mc_span_time( $group_id ) {
	$mcdb     = mc_is_remote_db();
	$group_id = (int) $group_id;
	$dates    = $mcdb->get_results( $mcdb->prepare( 'SELECT event_begin, event_time, event_end, event_endtime FROM ' . my_calendar_table() . ' WHERE event_group_id = %d ORDER BY event_begin ASC', $group_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	$count    = count( $dates );
	$last     = $count - 1;
	$begin    = $dates[0]->event_begin . ' ' . $dates[0]->event_time;
	$end      = $dates[ $last ]->event_end . ' ' . $dates[ $last ]->event_endtime;

	return array( $begin, $end );
}

/**
 * Generates the list of upcoming events when counting by events rather than a date pattern
 *
 * @param array  $events (Array of events to analyze).
 * @param array  $args Array of list arguments from calling function.
 * @param string $type Usually 'list', but also RSS or export.
 * @param string $context Display context.
 *
 * @return string; HTML output of list
 */
function mc_produce_upcoming_events( $events, $args, $type = 'list', $context = 'filters' ) {
	$template       = $args['template'];
	$order          = $args['template'];
	$skip           = $args['skip'];
	$before         = $args['before'];
	$after          = $args['after'];
	$show_recurring = $args['show_recurring'];
	// $events has +5 before and +5 after if those values are non-zero.
	// $events equals array of events based on before/after queries. Nothing skipped, order is not set, holiday conflicts removed.
	$output      = array();
	$near_events = array();
	$temp_array  = array();
	$past        = 0; // Number of events selected in the past.
	$future      = 0; // Number of events selected in the future.
	if ( '' === $args['time'] ) {
		uksort( $events, 'mc_timediff_cmp' ); // Sort all events by proximity to current date.
	} else {
		$compare_time = $args['time'];
		uksort(
			$events,
			function ( $a, $b ) use ( $compare_time ) {

				return mc_timediff_cmp( $a, $b, $compare_time );
			}
		);
	}
	$count = count( $events );
	$group = array();
	$spans = array();
	$occur = array();
	$extra = 0;
	$i     = 0;
	// Create near_events array.
	$recurring_events = array();
	$last_events      = array();
	$last_group       = array();
	if ( is_array( $events ) ) {
		foreach ( $events as $k => $event ) {
			if ( $i < $count ) {
				if ( is_array( $event ) ) {
					foreach ( $event as $e ) {
						if ( ! mc_private_event( $e ) ) {
							$beginning = $e->occur_begin;
							$end       = $e->occur_end;
							// Store span time in an array to avoid repeating database query.
							if ( '1' === $e->event_span && ( ! isset( $spans[ $e->occur_group_id ] ) ) ) {
								// This is a multi-day event: treat each event as if it spanned the entire range of the group.
								$span_time                   = mc_span_time( $e->occur_group_id );
								$beginning                   = $span_time[0];
								$end                         = $span_time[1];
								$spans[ $e->occur_group_id ] = $span_time;
							} elseif ( '1' === $e->event_span && ( isset( $spans[ $e->occur_group_id ] ) ) ) {
								$span_time = $spans[ $e->occur_group_id ];
								$beginning = $span_time[0];
								$end       = $span_time[1];
							}
							$current = current_time( 'Y-m-d H:i:00' );
							if ( $e ) {
								// If a multi-day event, show only once.
								if ( '0' !== $e->occur_group_id && '1' === $e->event_span && in_array( $e->occur_group_id, $group, true ) || in_array( $e->occur_id, $occur, true ) ) {
									$md = true;
								} else {
									$group[] = $e->occur_group_id;
									$occur[] = $e->occur_id;
									$md      = false;
								}
								// end multi-day reduction.
								if ( ! $md ) {
									if ( in_array( $e->occur_event_id, $recurring_events, true ) ) {
										$is_recurring = true;
									} else {
										$is_recurring = false;
										$instances    = mc_get_occurrences( $e->occur_event_id );
										if ( count( $instances ) > 1 ) {
											$recurring_events[] = $e->occur_event_id;
										}
									}
									if ( ( 'yes' !== $show_recurring ) && $is_recurring ) {
										continue;
									}
									$event_used = false;
									if ( my_calendar_date_equal( $beginning, $current ) ) {
										/**
										 * Should today's events be counted towards total number of upcoming events. Default `yes`. Any value other than 'no' will be interpreted as 'yes'.
										 *
										 * @hook mc_include_today_in_total
										 *
										 * @param {string} $in_total Return 'no' to exclude today's events from event count. Default 'yes'.
										 *
										 * @return {string} 'yes' or 'no'.
										 */
										$in_total = apply_filters( 'mc_include_today_in_total', 'yes' ); // count todays events in total.
										if ( 'yes' === $in_total ) {
											$near_events[] = $e;
											$event_used    = true;
											// Should today's events be counted as future or past? If more past events chosen, count as past.
											if ( $before > $after ) {
												++$past;
											} else {
												++$future;
											}
										} else {
											$near_events[] = $e;
											$event_used    = true;
										}
									}
									if ( '' !== $args['time'] ) {
										$current = $args['time'];
									}
									if ( $past <= $before && ( my_calendar_date_comp( $beginning, $current ) ) && ! $event_used ) {
										$near_events[] = $e; // Split off another past event.
										$event_used    = true;
									}
									if ( $future <= $after && ( ! my_calendar_date_comp( $end, $current ) ) && ! $event_used ) {
										$near_events[] = $e; // Split off another future event.
										$event_used    = true;
									}

									$event_added = false;
									// If this event happened before the current date.
									if ( my_calendar_date_comp( $beginning, $current ) ) {
										++$past;
										$event_added = true;
									}
									// If this happened on the current date.
									if ( my_calendar_date_equal( $beginning, $current ) && ! $event_added ) {
										++$extra;
										$event_added = true;
									}
									// If this did not end before the current date.
									if ( ! my_calendar_date_comp( $end, $current ) && ! $event_added ) {
										++$future;
										$event_added = true;
									}
									$last_events[] = $e->occur_id;
									$last_group[]  = $e->occur_group_id;
								}
								if ( $past > $before && $future > $after ) {
									break;
								}
							}
						}
					}
				}
			}
		}
	}

	$events = $near_events;
	usort( $events, 'mc_datetime_cmp' ); // Sort split events by date.

	if ( is_array( $events ) ) {
		foreach ( array_keys( $events ) as $key ) {
			$event        =& $events[ $key ];
			$temp_array[] = $event;
		}
		$i      = 0;
		$groups = array();
		$skips  = array();

		foreach ( reverse_array( $temp_array, true, $order ) as $event ) {
			$details = mc_create_tags( $event, $context );
			if ( ! in_array( $details['group'], $groups, true ) ) {
				// dtstart is already in current time zone.
				if ( $i < $skip && 0 !== $skip ) {
					++$i;
				} else {
					if ( ! in_array( $details['dateid'], $skips, true ) ) {
						$output[] = array(
							'event' => $event,
							'tags'  => $details,
						);
						$skips[]  = $details['dateid'];
					}
				}
				if ( '1' === $details['event_span'] ) {
					$groups[] = $details['group'];
				}
			}
		}
	}
	// If more items than there should be (due to handling of current-day's events), pop off.
	$intended = $before + $after + $extra;
	$actual   = count( $output );
	if ( $actual > $intended ) {
		for ( $i = 0; $i < ( $actual - $intended ); $i++ ) {
			array_pop( $output );
		}
	}

	$html       = '';
	$first_date = false;
	$last_date  = false;
	$i          = 1;
	foreach ( $output as $out ) {
		$event = $out['event'];
		$tags  = $out['tags'];
		$data  = array(
			'event'    => $event,
			'tags'     => $tags,
			'template' => $template,
			'type'     => $type,
			'time'     => 'list',
			'class'    => ( str_contains( $template, 'list_preset_' ) ) ? "list-preset $template" : '',
		);
		// Get first ID in set.
		if ( ! $first_date ) {
			$first_date = $event->occur_id;
		}
		if ( count( $output ) === $i ) {
			$last_date = $event->occur_id;
		}
		if ( 'card' === $template ) {
			$details = '<li class="card-event"><h3>' . mc_load_template( 'event/card-title', $data ) . '</h3>' . mc_load_template( 'event/card', $data ) . '</li>';
		} else {
			$details = mc_load_template( 'event/upcoming', $data );
		}
		if ( ! $details ) {
			$html .= mc_format_upcoming_event( $out, $template, $type );
		} else {
			$html .= $details;
		}
		++$i;
	}
	if ( ( $last_date || $first_date ) && 'events' === $args['type'] ) {
		$args['offset'] = count( $output ) - 1;
		$buttons        = mc_upcoming_events_navigation( $args, $first_date, $last_date );
		$html           = $buttons . $html;
	}

	return $html;
}

/**
 * Generate upcoming events navigation for dates.
 *
 * @param array $args Array of Upcoming events arguments.
 *
 * @return string
 */
function mc_upcoming_dates_navigation( $args ) {
	if ( 'true' !== $args['navigation'] ) {
		return '';
	}
	if ( ! isset( $args['from'] ) || ! $args['from'] ) {
		$args['from'] = gmdate( 'Y-m-d', time() - DAY_IN_SECONDS * $args['before'] );
		$args['to']   = gmdate( 'Y-m-d', time() + DAY_IN_SECONDS * $args['after'] );
	}

	$diff     = strtotime( $args['to'] ) - strtotime( $args['from'] );
	$new_from = gmdate( 'Y-m-d', strtotime( $args['from'] ) - $diff );
	$new_to   = gmdate( 'Y-m-d', strtotime( $args['to'] ) + $diff );
	$to       = $args['to'];
	$from     = $args['from'];

	$args['to']     = $from;
	$args['from']   = $new_from;
	$json_args_prev = str_replace( '&', '|', http_build_query( $args ) );

	$args['to']     = $new_to;
	$args['from']   = $to;
	$json_args_next = str_replace( '&', '|', http_build_query( $args ) );

	return '<li class="mc-load-events-controls">
				<button class="mc-loader mc-load-prev-upcoming-dates mc-previous" type="button" data-value="' . esc_attr( $json_args_prev ) . '" value="dates"><span class="mc-icon" aria-hidden="true"></span><span class="mc-text">' . esc_html__( 'Previous Events', 'my-calendar' ) . '</span></button>
				<button class="mc-loader mc-load-next-upcoming-dates mc-next" type="button" data-value="' . esc_attr( $json_args_next ) . '" value="dates"><span class="mc-text">' . esc_html__( 'Future Events', 'my-calendar' ) . '</span><span class="mc-icon" aria-hidden="true"></span></button>
			</li>';
}

/**
 * Generate upcoming events navigation buttons.
 *
 * @param array    $args Upcoming Events arguments.
 * @param int|bool $first_date Occurrence ID of previous event.
 * @param int|bool $last_date  Occurrence ID of next event.
 *
 * @return string
 */
function mc_upcoming_events_navigation( $args, $first_date, $last_date ) {
	if ( 'true' !== $args['navigation'] ) {
		return '';
	}
	unset( $args['time'] );
	$json_args   = str_replace( '&', '|', http_build_query( $args ) );
	$prev_button = '';
	$next_button = '';
	if ( $first_date ) {
		$args['return'] = 'object';
		$prev           = mc_adjacent_event( $first_date, 'previous', $args );
		if ( is_object( $prev ) ) {
			$prev_date = $prev->occur_begin;
			$label     = __( 'Previous Events', 'my-calendar' );
			$class     = 'mc-previous';
		} else {
			$prev_date = '';
			$label     = __( 'Today', 'my-calendar' );
			$class     = 'mc-today';
		}
		$prev_button .= '<button class="mc-loader mc-load-prev-upcoming-events ' . esc_attr( $class ) . '" type="button" data-value="' . esc_attr( $json_args ) . '" value="' . esc_attr( $prev_date ) . '"><span class="mc-icon" aria-hidden="true"></span><span class="mc-text">' . esc_html( $label ) . '</span></button>';
	}
	if ( $last_date ) {
		unset( $args['offset'] );
		$args['return'] = 'object';
		$next           = mc_adjacent_event( $last_date, 'next', $args );
		if ( is_object( $next ) ) {
			$next_date = $next->occur_begin;
			$label     = __( 'Future Events', 'my-calendar' );
			$class     = 'mc-next';
		} else {
			$next_date = '';
			$label     = __( 'Today', 'my-calendar' );
			$class     = 'mc-today';
		}
		$next_button .= '<button class="mc-loader mc-load-next-upcoming-events ' . esc_attr( $class ) . '" type="button" data-value="' . esc_attr( $json_args ) . '" value="' . esc_attr( $next_date ) . '"><span class="mc-text">' . esc_html( $label ) . '</span><span class="mc-icon" aria-hidden="true"></span></button>';
	}
	$buttons = ( $prev_button || $next_button ) ? '<li class="mc-load-events-controls">' . $prev_button . $next_button . '</li>' : '';

	return $buttons;
}

/**
 * Process the Today's Events widget.
 *
 * @param array $args Event & output construction parameters.
 *
 * @return string HTML.
 */
function my_calendar_todays_events( $args ) {
	$language = isset( $args['language'] ) ? $args['language'] : '';
	$switched = '';
	if ( $language ) {
		$locale   = get_locale();
		$switched = mc_switch_language( $locale, $language );
	}

	$category   = ( isset( $args['category'] ) ) ? $args['category'] : 'default';
	$template   = ( isset( $args['template'] ) ) ? $args['template'] : 'default';
	$substitute = ( isset( $args['fallback'] ) ) ? $args['fallback'] : '';
	$author     = ( isset( $args['author'] ) ) ? $args['author'] : 'all';
	$host       = ( isset( $args['host'] ) ) ? $args['host'] : 'all';
	$date       = ( isset( $args['date'] ) ) ? $args['date'] : false;
	$site       = ( isset( $args['site'] ) ) ? $args['site'] : false;

	if ( $site ) {
		$site = ( 'global' === $site ) ? BLOG_ID_CURRENT_SITE : $site;
		switch_to_blog( $site );
	}

	$params = array(
		'category'   => $category,
		'template'   => $template,
		'substitute' => $substitute,
		'author'     => $author,
		'host'       => $host,
		'date'       => $date,
	);
	$hash   = md5( implode( ',', $params ) );
	$output = '';

	$defaults = mc_widget_defaults();
	$default  = ( ! $template || 'default' === $template ) ? $defaults['today']['template'] : $template;
	$template = mc_setup_template( $template, $default );

	$category      = ( 'default' === $category ) ? $defaults['today']['category'] : $category;
	$no_event_text = ( '' === $substitute ) ? $defaults['today']['text'] : $substitute;
	if ( $date ) {
		$from = mc_date( 'Y-m-d', strtotime( $date ), false );
		$to   = mc_date( 'Y-m-d', strtotime( $date ), false );
	} else {
		$from = current_time( 'Y-m-d' );
		$to   = current_time( 'Y-m-d' );
	}

	$args = array(
		'from'     => $from,
		'to'       => $to,
		'category' => $category,
		'ltype'    => '',
		'lvalue'   => '',
		'author'   => $author,
		'host'     => $host,
		'search'   => '',
		'source'   => 'upcoming',
		'site'     => $site,
	);
	/**
	 * Modify the arguments used to generate today's events.
	 *
	 * @hook mc_today_attributes
	 *
	 * @param {array} $args All arguments used to generate this list.
	 * @param {array} $params Subset of parameters used to generate this list's ID hash.
	 *
	 * @return {array} Array of event listing arguments.
	 */
	$args   = apply_filters( 'mc_today_attributes', $args, $params );
	$events = my_calendar_events( $args );

	$today         = ( isset( $events[ $from ] ) ) ? $events[ $from ] : false;
	$lang          = ( $switched ) ? ' lang="' . esc_attr( $switched ) . '"' : '';
	$class         = ( 'card' === $template ) ? 'my-calendar-cards' : 'list-events';
	$header        = "<ul id='todays-events-$hash' class='mc-event-list todays-events $class'$lang>";
	$footer        = '</ul>';
	$groups        = array();
	$todays_events = array();
	// quick loop through all events today to check for holidays.
	if ( is_array( $today ) ) {
		foreach ( $today as $e ) {
			if ( ! mc_private_event( $e ) && ! in_array( $e->event_group_id, $groups, true ) ) {
				$event_details = mc_create_tags( $e );
				$ts            = $e->ts_occur_begin;
				$classes       = mc_get_event_classes( $e, 'today' );

				$data = array(
					'event'    => $e,
					'tags'     => $event_details,
					'template' => $template,
					'args'     => $args,
					'class'    => ( str_contains( $template, 'list_preset_' ) ) ? "list-preset $template" : '',
				);
				if ( 'card' === $template ) {
					$details = '<li class="card-event"><h3>' . mc_load_template( 'event/card-title', $data ) . '</h3>' . mc_load_template( 'event/card', $data ) . '</li>';
				} else {
					$details = mc_load_template( 'event/today', $data );
				}
				if ( $details ) {
					$todays_events[ $ts ][] = $details;
				} else {
					/**
					 * Modify the HTML preceding each list item in a list of today's events.
					 *
					 * @hook mc_todays_events_before
					 *
					 * @param {string} $item HTML string before each event.
					 * @param {string} $classes Space separated list of classes for this event.
					 * @param {string} $category Category argument passed to this list.
					 *
					 * @return {string} HTML preceding each event in today's events lists.
					 */
					$prepend = apply_filters( 'mc_todays_events_before', "<li class='$classes'>", $classes, $category );
					/**
					 * Closing elements for today's events list items. Default `</li>`.
					 *
					 * @hook mc_todays_events_after
					 *
					 * @param {string} $item Template HTML closing tag.
					 *
					 * @return {string} HTML following each event in today's events lists.
					 */
					$append = apply_filters( 'mc_todays_events_after', '</li>' );

					/**
					 * Draw a custom template for today's events. Returning any non-empty string short circuits other template settings.
					 *
					 * @hook mc_draw_todays_event
					 *
					 * @param {string} $item Empty string before event template is drawn.
					 * @param {array}  $event_details Associative array of event template tags.
					 * @param {string} $template Template string passed from widget or shortcode.
					 * @param {array}  $args Associative array holding the arguments used to generate this list of events.
					 *
					 * @return {string} Event output details.
					 */
					$item = apply_filters( 'mc_draw_todays_event', '', $event_details, $template, $args );
					if ( '' === $item ) {
						$item = mc_draw_template( $event_details, $template, 'list', $e );
					}
					$todays_events[ $ts ][] = $prepend . $item . $append;
				}
			}
		}
		/**
		 * Filter the array of events listed in today's events lists.
		 *
		 * @hook mc_event_today
		 *
		 * @param {array} $todays_events  A multidimensional array of event items with today's date as a key with an array of formatted HTML on event templates on the current date.
		 * @param {array} $events Array of events without private events removed. Values are event objects.
		 *
		 * @return {array} A multidimensional array of event items with today's date as a key with an array of formatted HTML on event templates on the current date.
		 */
		$todays_events = apply_filters( 'mc_event_today', $todays_events, $events );
		foreach ( $todays_events as $k => $t ) {
			foreach ( $t as $now ) {
				$output .= $now;
			}
		}
		if ( 0 !== count( $events ) ) {
			/**
			 * Replace the list header for today's events lists. Default value `<ul id='todays-events-$hash' class='mc-event-list todays-events'$lang>`.
			 *
			 * @hook mc_todays_events_header
			 *
			 * @param {string} $header Existing today's events header HTML.
			 *
			 * @return {string} List header HTML.
			 */
			$return  = apply_filters( 'mc_todays_events_header', $header );
			$return .= $output;
			/**
			 * Replace the list footer for today's events lists. Default value `</ul>`.
			 *
			 * @hook mc_todays_events_header
			 *
			 * @param {string} $header Existing today's events header HTML.
			 *
			 * @return {string} List header HTML.
			 */
			$return .= apply_filters( 'mc_todays_events_footer', $footer );
		} else {
			$return = '<div class="no-events-fallback todays-events">' . stripcslashes( $no_event_text ) . '</div>';
		}
	} else {
		$return = '<div class="no-events-fallback todays-events">' . stripcslashes( $no_event_text ) . '</div>';
	}

	if ( $site ) {
		restore_current_blog();
	}

	$output = mc_run_shortcodes( $return );

	if ( $language ) {
		mc_switch_language( $language, $locale );
	}

	return '<div class="mc-event-list-container">' . $output . '</div>';
}