status) { return ''; } return '' . esc_html__('Cancelled', '3task-calendar') . ' '; } /** * "Date to be announced" note for an event of which only the month is known. * * @since 1.6.0 * @param ThreeCal_Event $event Event. * @return string Escaped HTML, empty for events with a known day. */ public static function tba_note($event) { if (!$event || !$event->is_month_only()) { return ''; } return '' . esc_html(ThreeCal_Event::tba_text()) . ''; } /** * Settings */ private $settings; /** * Constructor */ public function __construct() { $this->settings = get_option('threecal_settings', array()); } /** * Date format from the plugin settings or WordPress. */ private function date_format() { return threecal_date_format(); } /** * Time format from the plugin settings or WordPress. */ private function time_format() { return threecal_time_format(); } /** * Attributes of the link to an event's URL, filterable per event. * * @since 1.6.3 * * @param ThreeCal_Event $event The event. * @param string $context Where the link is printed: card, poster, single, upcoming. * @param array $attrs Attributes the template already sets (class, target, rel ...). * @return string Escaped HTML attributes, href included. */ public static function link_attributes($event, $context, $attrs = array()) { $attrs = array_merge($attrs, array('href' => $event->url)); $attrs = self::apply_link_rule($attrs, $event); /** * Filters the attributes of the link to an event's URL. * * Return for example rel => 'nofollow noopener' and target => '_blank' for * events of other organisers. Since 1.6.3 * * @param array $attrs Attributes (href, class, target, rel ...). * @param ThreeCal_Event $event The event. * @param string $context card, poster, single, upcoming or details. */ $attrs = apply_filters('threecal_event_link_attributes', $attrs, $event, $context); $out = array(); foreach ((array) $attrs as $name => $value) { if (!preg_match('/^[a-z][a-z0-9-]*$/', (string) $name) || null === $value || false === $value) { continue; } $out[] = esc_attr($name) . '="' . ('href' === $name ? esc_url($value) : esc_attr($value)) . '"'; } return implode(' ', $out); } /** * Link rule for links to other websites (since 1.7.0). * * Links to the own site stay as they are. Other links open in a new window and are * nofollow, unless the setting "Links to other websites" says follow or one of the * event's categories is marked as follow. * * @param array $attrs Attributes with href. * @param ThreeCal_Event $event The event. * @return array */ public static function apply_link_rule($attrs, $event) { $href = isset($attrs['href']) ? (string) $attrs['href'] : ''; $host = wp_parse_url($href, PHP_URL_HOST); if (!$host) { return $attrs; } $own = (string) wp_parse_url(home_url('/'), PHP_URL_HOST); if (0 === strcasecmp(preg_replace('/^www\./i', '', $host), preg_replace('/^www\./i', '', $own))) { return $attrs; } $settings = get_option('threecal_settings', array()); $follow = isset($settings['external_links']) && 'follow' === $settings['external_links']; if (!$follow && !empty($event->id)) { $follow_ids = ThreeCal_Category::follow_ids(); if ($follow_ids) { foreach ((array) ThreeCal_Event::get_categories($event->id) as $cat) { if (in_array((int) $cat->id, $follow_ids, true)) { $follow = true; break; } } } } $rel = isset($attrs['rel']) ? preg_split('/\s+/', trim((string) $attrs['rel'])) : array(); $rel = array_values(array_diff($rel, array('nofollow', 'noopener'))); if (!$follow) { $rel[] = 'nofollow'; } $rel[] = 'noopener'; $attrs['rel'] = implode(' ', $rel); $attrs['target'] = '_blank'; return $attrs; } /** * Highlighted category of an event (first match), null if none. Since 1.7.0 * * @param array $categories Category rows of the event. * @return object|null */ public static function highlight_category($categories) { $ids = ThreeCal_Category::highlighted_ids(); if (!$ids) { return null; } foreach ((array) $categories as $cat) { if (in_array((int) $cat->id, $ids, true)) { return $cat; } } return null; } /** * Badge of a highlighted event: star and name of the category. Since 1.7.0 * * @param array $categories Category rows of the event. * @return string Escaped HTML or empty string. */ public static function highlight_badge($categories) { $cat = self::highlight_category($categories); if (!$cat) { return ''; } ob_start(); ThreeCal_Icons::render('star', 12); $icon = ob_get_clean(); return '' . $icon . esc_html($cat->name) . ''; } /** * Classes of an event card, filterable per event. * * @since 1.6.3 * * @param array $classes Classes the template sets. * @param ThreeCal_Event $event The event. * @param string $view list, compact, poster. * @param array $categories Category rows of the event (since 1.7.0, for the highlight class). * @return string Escaped class attribute value. */ public static function card_classes($classes, $event, $view, $categories = array()) { if (self::highlight_category($categories)) { $classes[] = 'threecal-is-highlight'; } /** * Filters the CSS classes of an event card. Since 1.6.3 * * @param array $classes Classes. * @param ThreeCal_Event $event The event. * @param string $view list, compact, poster. */ $classes = apply_filters('threecal_event_card_classes', array_values(array_filter((array) $classes)), $event, $view); return esc_attr(implode(' ', array_unique(array_map('sanitize_html_class', (array) $classes)))); } /** * Search field (since 1.8.0). * * In the calendar the field is handled by JavaScript (same public request as the month * navigation). In the event list it is a plain GET form, so it works without JavaScript * and page caches keep working: every search term is its own address. * * @param array $args id (prefix for the field id), value (current term), param (query * parameter), method ('get' for a real form, '' for the calendar), * action (URL of the form), keep (query parameters to carry along). * @return string */ public static function render_search_form($args = array()) { $args = wp_parse_args($args, array( 'id' => 'threecal-' . wp_rand(1000, 9999), 'value' => '', 'param' => 'tc_q', 'method' => '', 'action' => '', 'keep' => array(), )); $field_id = $args['id'] . '-search'; $out = ''; return $out; } /** * Render full calendar */ public function render_calendar($args = array()) { $defaults = array( 'view' => 'month', 'category_id' => 0, 'location_id' => 0, 'theme' => '', 'show_filters' => true, 'show_legend' => true, 'show_subscribe' => true, 'show_print' => true, 'show_search' => true, 'mobile_list' => true, 'week_starts_on' => null ); $args = wp_parse_args($args, $defaults); ThreeCal_Themes::remember_page_theme($args['theme']); $args['theme'] = ThreeCal_Themes::normalize($args['theme']); $args['view'] = in_array($args['view'], array('month', 'list'), true) ? $args['view'] : 'month'; ThreeCal_Themes::enqueue($args['theme']); // Get week start from settings if not specified if ($args['week_starts_on'] === null) { $args['week_starts_on'] = isset($this->settings['week_starts_on']) ? (int) $this->settings['week_starts_on'] : 1; } // Get current month/year. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Calendar navigation params, sanitized with absint(). $month = isset( $_GET['cc_month'] ) ? absint( $_GET['cc_month'] ) : (int) current_time( 'n' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Calendar navigation params, sanitized with absint(). $year = isset( $_GET['cc_year'] ) ? absint( $_GET['cc_year'] ) : (int) current_time( 'Y' ); // Validate month/year if ($month < 1 || $month > 12) { $month = (int) current_time('n'); } if ($year < 2000 || $year > 2100) { $year = (int) current_time('Y'); } // Get categories for filter/legend $categories = ThreeCal_Category::get_all(); // Generate unique ID for this calendar instance $calendar_id = 'threecal-' . wp_rand(1000, 9999); ob_start(); ?>
get_month_name($month)); ?>
1; ?>
$calendar_id)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in render_search_form(). Since 1.8.0, next to the category filter. ?>
render_month_view( $month, $year, $args ); ?>
ThreeCal_Event::visible_statuses(), 'range_start' => gmdate('Y-m-d 00:00:00', $first_day), 'range_end' => gmdate('Y-m-t 23:59:59', $first_day) ); if (!empty($args['category_id'])) { $event_args['category_id'] = $args['category_id']; } if (!empty($args['location_id'])) { $event_args['location_id'] = $args['location_id']; } $events = ThreeCal_Event::get_all($event_args); $categories = ThreeCal_Event::get_categories_for_events(wp_list_pluck($events, 'id')); $time_format = $this->time_format(); // Group events by every day they cover (multi-day events appear on each day) $events_by_day = array(); $month_only = array(); foreach ($events as $event) { // Only the month is known: shown in a bar above the grid, not on the 1st. if ($event->is_month_only()) { $month_only[] = array( 'event' => $event, 'color' => ThreeCal_Themes::event_color($event, isset($categories[$event->id]) ? $categories[$event->id] : array()), ); continue; } foreach ($event->get_days_in_month($month, $year) as $day) { $events_by_day[$day][] = array( 'event' => $event, 'color' => ThreeCal_Themes::event_color($event, isset($categories[$event->id]) ? $categories[$event->id] : array()), 'starts_here' => substr($event->start_date, 0, 10) === sprintf('%04d-%02d-%02d', $year, $month, $day), ); } } $today = current_time('Y-m-d'); $total_cells = $first_weekday + $days_in_month; $weeks = (int) ceil($total_cells / 7); $day = 1; ob_start(); ?>
title); ?>
$days_in_month) : ?>
get_weekday_name(($i + $week_starts_on) % 7)); ?>
$item) : $event = $item['event']; $show_time = !$event->all_day && $item['starts_here']; ?> start_date))); ?> title); ?> 3) : ?> +
'list', 'theme' => '', 'show_pagination' => true, 'columns' => 3, 'total' => 0, 'per_page' => 10, 'current_page' => 1, 'page_param' => 'tc_page', // Search in the event list (since 1.8.0): the form above the list, the current term, // the address without the term, and whether the cards are grouped by day. 'search_form' => '', 'search_term' => '', 'reset_url' => '', 'group_days' => false, ); $args = wp_parse_args($args, $defaults); $args['theme'] = ThreeCal_Themes::normalize($args['theme']); $args['view'] = in_array($args['view'], array('list', 'grid', 'compact', 'poster'), true) ? $args['view'] : 'list'; $args['columns'] = min(6, max(1, (int) $args['columns'])); ThreeCal_Themes::enqueue($args['theme']); $searching = '' !== (string) $args['search_term']; if (empty($events) && !$searching && '' === $args['search_form']) { return '

' . esc_html__('No events found.', '3task-calendar') . '

'; } $categories = $events ? ThreeCal_Event::get_categories_for_events(wp_list_pluck($events, 'id')) : array(); $cards_class = 'threecal-cards' . ($args['view'] === 'grid' ? ' threecal-grid threecal-grid-' . $args['columns'] : '') . ($args['view'] === 'poster' ? ' threecal-poster-grid' : ''); $cards_style = $args['view'] === 'poster' ? ' style="--tc-cols:' . esc_attr($args['columns']) . ';"' : ''; // Cards grouped by day when wanted: one group per day (or per month for month-only dates). $groups = array(); foreach ((array) $events as $event) { $key = 'all'; if ($args['group_days']) { $key = $event->is_month_only() ? 'm' . substr((string) $event->start_date, 0, 7) : 'd' . substr((string) $event->start_date, 0, 10); } if (!isset($groups[$key])) { $groups[$key] = array( 'title' => $args['group_days'] ? ($event->is_month_only() ? $event->month_label() : date_i18n(ThreeCal::day_heading_format(), strtotime($event->start_date))) : '', 'events' => array(), ); } $groups[$key]['events'][] = $event; } ob_start(); ?>

> id ] ) ? $categories[ $event->id ] : array(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped within the card methods. echo 'poster' === $args['view'] ? $this->render_poster_card( $event, $event_cats ) : $this->render_event_card( $event, $args['view'], $event_cats ); ?>
$args['per_page'] ) : ?>
render_pagination( $args['total'], $args['per_page'], $args['current_page'], $args['page_param'] ); ?>
start_date); $month_only = $event->is_month_only(); if ($month_only) { $when = $event->month_label(); } else { $when = date_i18n('D', $start) . ', ' . date_i18n($this->date_format(), $start); if (!$event->all_day) { $when .= ' · ' . date_i18n($this->time_format(), $start); } } $postponed = ThreeCal_Post_Dates::postponed_text($event); $cancelled = 'cancelled' === $event->status; $link = $event->url; ob_start(); ?>
'threecal-poster-media', 'tabindex' => '-1', 'aria-hidden' => 'true')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in link_attributes(). ?>>
featured_image) : ?> featured_image, 'medium_large', false, array('loading' => 'lazy', 'alt' => '')); ?> ' : '
'; ?>

>title); ?> title); ?>

location_id ? ThreeCal_Location::get($event->location_id) : null; if (null === $categories) { $categories = ThreeCal_Event::get_categories($event->id); } $color = ThreeCal_Themes::event_color($event, $categories); $date_format = $this->date_format(); $time_format = $this->time_format(); $start = strtotime($event->start_date); $month_only = $event->is_month_only(); ob_start(); ?>
featured_image && 'compact' !== $view) : ?>
featured_image, 'medium_large', false, array('loading' => 'lazy')); ?>
name); ?>

url) : ?> >title); ?> title); ?>

month_label()) . ' ' . self::tba_note($event); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in tba_note(). } else { echo esc_html(date_i18n($date_format, $start)); if (!$event->all_day) { echo ' · ' . esc_html(date_i18n($time_format, $start)); } if ($event->end_date && $event->end_date !== $event->start_date) { $end = strtotime($event->end_date); echo ' ' . esc_html_x('to', 'between two dates or times', '3task-calendar') . ' '; if (gmdate('Y-m-d', $start) !== gmdate('Y-m-d', $end)) { echo esc_html(date_i18n($date_format, $end)); if (!$event->all_day) { echo ' · '; } } if (!$event->all_day) { echo esc_html(date_i18n($time_format, $end)); } } if ($event->all_day) { echo ' · ' . esc_html__('All day', '3task-calendar'); } } ?> name); ?>city && $location->city !== $location->name) : ?>, city); ?>

description ) ) : ?>

description ), 24 ) ); ?>

'', 'show_map' => true, 'show_description' => true ); $args = wp_parse_args($args, $defaults); $args['theme'] = ThreeCal_Themes::normalize($args['theme']); ThreeCal_Themes::enqueue($args['theme']); $location = $event->location_id ? ThreeCal_Location::get($event->location_id) : null; $categories = ThreeCal_Event::get_categories($event->id); $color = ThreeCal_Themes::event_color($event, $categories); $date_format = $this->date_format(); $time_format = $this->time_format(); ob_start(); ?>
featured_image) : ?>
name); ?>

title); ?>


is_month_only()) { echo esc_html($event->month_label()) . ' ' . self::tba_note($event); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in tba_note(). } else { echo esc_html(date_i18n($date_format, strtotime($event->start_date))); if ($event->end_date && gmdate('Y-m-d', strtotime($event->start_date)) !== gmdate('Y-m-d', strtotime($event->end_date))) { echo ' ' . esc_html_x('to', 'between two dates or times', '3task-calendar') . ' ' . esc_html(date_i18n($date_format, strtotime($event->end_date))); } } ?>
all_day) : ?>

start_date))); if ($event->end_date) { echo ' ' . esc_html_x('to', 'between two dates or times', '3task-calendar') . ' ' . esc_html(date_i18n($time_format, strtotime($event->end_date))); } ?>
name); ?>
get_full_address()); ?> get_route_url()) : ?>
description)) : ?>
description)); ?>
is_month_only()) : ?> url) : ?> 'threecal-button threecal-button-ghost', 'target' => '_blank', 'rel' => 'noopener')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in link_attributes(). ?>>
'', 'show_date' => true, 'show_time' => true, 'show_location' => true ); $args = wp_parse_args($args, $defaults); $args['theme'] = ThreeCal_Themes::normalize($args['theme']); ThreeCal_Themes::enqueue($args['theme']); $date_format = $this->date_format(); $time_format = $this->time_format(); if (empty($events)) { return '

' . esc_html__('No upcoming events.', '3task-calendar') . '

'; } $categories = ThreeCal_Event::get_categories_for_events(wp_list_pluck($events, 'id')); ob_start(); ?> 0, 'theme' => '', 'show_nav' => true, 'show_today' => true, 'week_starts_on' => null ); $args = wp_parse_args($args, $defaults); $args['theme'] = ThreeCal_Themes::normalize($args['theme']); ThreeCal_Themes::enqueue($args['theme']); // Get week start from settings if not specified if ($args['week_starts_on'] === null) { $args['week_starts_on'] = isset($this->settings['week_starts_on']) ? (int) $this->settings['week_starts_on'] : 1; } // Get current month/year from URL or use current date. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Calendar navigation params, sanitized with absint(). $month = isset($_GET['cc_mini_month']) ? absint($_GET['cc_mini_month']) : (int) current_time('n'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Calendar navigation params, sanitized with absint(). $year = isset($_GET['cc_mini_year']) ? absint($_GET['cc_mini_year']) : (int) current_time('Y'); // Validate month/year if ($month < 1 || $month > 12) { $month = (int) current_time('n'); } if ($year < 2000 || $year > 2100) { $year = (int) current_time('Y'); } $week_starts_on = (int) $args['week_starts_on']; $date_format = $this->date_format(); $time_format = $this->time_format(); // Get first day of month $first_day = mktime(0, 0, 0, $month, 1, $year); $days_in_month = (int) gmdate('t', $first_day); $first_weekday = ((int) gmdate('w', $first_day) - $week_starts_on + 7) % 7; $event_args = array( 'status' => ThreeCal_Event::visible_statuses(), 'range_start' => gmdate('Y-m-d 00:00:00', $first_day), 'range_end' => gmdate('Y-m-t 23:59:59', $first_day) ); if (!empty($args['category_id'])) { $event_args['category_id'] = $args['category_id']; } $events = ThreeCal_Event::get_all($event_args); $categories = ThreeCal_Event::get_categories_for_events(wp_list_pluck($events, 'id')); // Group events by every day they cover, with event data for the popup $events_data_by_day = array(); foreach ($events as $event) { // The mini calendar has no room for dates without a day. if ($event->is_month_only()) { continue; } $color = ThreeCal_Themes::event_color($event, isset($categories[$event->id]) ? $categories[$event->id] : array()); $event_time = $event->all_day ? __('All day', '3task-calendar') : date_i18n($time_format, strtotime($event->start_date)); foreach ($event->get_days_in_month($month, $year) as $day) { $events_data_by_day[$day][] = array( 'id' => $event->id, 'title' => $event->title, 'time' => $event_time, 'color' => $color, 'url' => $event->url ? esc_url_raw($event->url) : '' ); } } // Calculate navigation URLs $prev_month = $month - 1; $prev_year = $year; if ($prev_month < 1) { $prev_month = 12; $prev_year--; } $next_month = $month + 1; $next_year = $year; if ($next_month > 12) { $next_month = 1; $next_year++; } $current_url = remove_query_arg(array('cc_mini_month', 'cc_mini_year')); $prev_url = add_query_arg(array('cc_mini_month' => $prev_month, 'cc_mini_year' => $prev_year), $current_url); $next_url = add_query_arg(array('cc_mini_month' => $next_month, 'cc_mini_year' => $next_year), $current_url); $today_url = remove_query_arg(array('cc_mini_month', 'cc_mini_year'), $current_url); $calendar_id = 'threecal-mini-' . wp_rand(1000, 9999); $today = current_time('Y-m-d'); // Enqueue mini calendar CSS and JS. wp_enqueue_style( 'threecal-mini' ); wp_enqueue_script( 'threecal-mini' ); ob_start(); ?>
get_month_name($month) . ' ' . $year); ?>
$days_in_month) : ?>
get_weekday_initial(($i + $week_starts_on) % 7)); ?>
tabindex="0" role="button" aria-label="" data-date-formatted="" data-events="" >
'; if ($current_page > 1) { $output .= ''; $output .= '« ' . esc_html__('Previous', '3task-calendar'); $output .= ''; } // First, last and the pages around the current one; gaps become an ellipsis. $gap = false; for ($i = 1; $i <= $total_pages; $i++) { if ($i !== 1 && $i !== $total_pages && abs($i - $current_page) > 1) { if (!$gap) { $output .= ''; $gap = true; } continue; } $gap = false; if ($i === $current_page) { $output .= '' . $i . ''; } else { $output .= '' . $i . ''; } } if ($current_page < $total_pages) { $output .= ''; $output .= esc_html__('Next', '3task-calendar') . ' »'; $output .= ''; } $output .= ''; return $output; } /** * Get month name */ private function get_month_name($month) { $months = array( 1 => __('January', '3task-calendar'), 2 => __('February', '3task-calendar'), 3 => __('March', '3task-calendar'), 4 => __('April', '3task-calendar'), 5 => __('May', '3task-calendar'), 6 => __('June', '3task-calendar'), 7 => __('July', '3task-calendar'), 8 => __('August', '3task-calendar'), 9 => __('September', '3task-calendar'), 10 => __('October', '3task-calendar'), 11 => __('November', '3task-calendar'), 12 => __('December', '3task-calendar') ); return isset($months[$month]) ? $months[$month] : ''; } /** * Get weekday name (short) */ private function get_weekday_name($day) { $days = array( 0 => __('Sun', '3task-calendar'), 1 => __('Mon', '3task-calendar'), 2 => __('Tue', '3task-calendar'), 3 => __('Wed', '3task-calendar'), 4 => __('Thu', '3task-calendar'), 5 => __('Fri', '3task-calendar'), 6 => __('Sat', '3task-calendar') ); return isset($days[$day]) ? $days[$day] : ''; } /** * Get weekday initial (single letter for mini calendar) */ private function get_weekday_initial($day) { $days = array( /* translators: Single letter abbreviation for Sunday */ 0 => _x('S', 'Sunday initial', '3task-calendar'), /* translators: Single letter abbreviation for Monday */ 1 => _x('M', 'Monday initial', '3task-calendar'), /* translators: Single letter abbreviation for Tuesday */ 2 => _x('T', 'Tuesday initial', '3task-calendar'), /* translators: Single letter abbreviation for Wednesday */ 3 => _x('W', 'Wednesday initial', '3task-calendar'), /* translators: Single letter abbreviation for Thursday */ 4 => _x('T', 'Thursday initial', '3task-calendar'), /* translators: Single letter abbreviation for Friday */ 5 => _x('F', 'Friday initial', '3task-calendar'), /* translators: Single letter abbreviation for Saturday */ 6 => _x('S', 'Saturday initial', '3task-calendar') ); return isset($days[$day]) ? $days[$day] : ''; } }