✨ feature: Add calendar layout and occurrence handling functionality
- Introduced `calendar-layout.php` to manage month grid generation, occurrence intersection checks, and segment assignments for events. - Added `occurrences.php` for normalizing, validating, and displaying event occurrences, including migration from legacy formats. - Implemented functions for parsing dates, validating occurrence ranges, and formatting occurrences for display. - Established hooks for synchronizing event occurrence data upon saving posts and validating ACF repeater fields.
This commit is contained in:
+185
-2
@@ -9,10 +9,74 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an ACF field group created in the admin targets the event post type.
|
||||
*
|
||||
* @return bool True if an external field group is assigned to events.
|
||||
*/
|
||||
function events_has_admin_field_group() {
|
||||
if ( ! function_exists( 'acf_get_field_groups' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = acf_get_field_groups( array( 'post_type' => 'event' ) );
|
||||
|
||||
foreach ( $groups as $group ) {
|
||||
if ( isset( $group['active'] ) && ! $group['active'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $group['key'], $group['local'] )
|
||||
&& 'group_61b0c5f5a3e7e' === $group['key']
|
||||
&& 'php' === $group['local']
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets_events = false;
|
||||
foreach ( $group['location'] ?? array() as $location_group ) {
|
||||
foreach ( $location_group as $rule ) {
|
||||
if (
|
||||
isset( $rule['param'], $rule['operator'], $rule['value'] )
|
||||
&& 'post_type' === $rule['param']
|
||||
&& '==' === $rule['operator']
|
||||
&& 'event' === $rule['value']
|
||||
) {
|
||||
$targets_events = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $targets_events || ! function_exists( 'acf_get_fields' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( (array) acf_get_fields( $group ) as $field ) {
|
||||
if (
|
||||
isset( $field['name'] )
|
||||
&& in_array( $field['name'], array( 'event_datetime', 'event_dates', 'multiple_dates', 'date' ), true )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the ACF field group.
|
||||
*
|
||||
* Skips registration when an admin-created field group is already
|
||||
* assigned to the event post type, allowing full editorial control.
|
||||
*/
|
||||
function events_register_acf_fields() {
|
||||
if ( events_has_admin_field_group() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( function_exists( 'acf_add_local_field_group' ) ) :
|
||||
|
||||
acf_add_local_field_group(
|
||||
@@ -25,8 +89,8 @@ function events_register_acf_fields() {
|
||||
'label' => 'Event Date & Time',
|
||||
'name' => 'event_datetime',
|
||||
'type' => 'date_time_picker',
|
||||
'instructions' => '',
|
||||
'required' => 1,
|
||||
'instructions' => 'Legacy compatibility field. Use Occurrences for new events.',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '',
|
||||
@@ -56,6 +120,109 @@ function events_register_acf_fields() {
|
||||
'append' => '',
|
||||
'maxlength' => '',
|
||||
),
|
||||
array(
|
||||
'key' => 'field_61b0c7f0a3e8f',
|
||||
'label' => 'Occurrences',
|
||||
'name' => 'event_dates',
|
||||
'type' => 'repeater',
|
||||
'instructions' => 'Each row is one occurrence. Add an end date for a continuous multi-day event; add more rows when the event recurs.',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'collapsed' => '',
|
||||
'min' => 0,
|
||||
'max' => 0,
|
||||
'layout' => 'row',
|
||||
'button_label' => 'Add Occurrence',
|
||||
'sub_fields' => array(
|
||||
array(
|
||||
'key' => 'field_61b0c7f1a3e90',
|
||||
'label' => 'Date',
|
||||
'name' => 'date',
|
||||
'type' => 'date_picker',
|
||||
'instructions' => '',
|
||||
'required' => 1,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '25',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'display_format' => 'F j, Y',
|
||||
'return_format' => 'Y-m-d',
|
||||
'first_day' => 1,
|
||||
),
|
||||
array(
|
||||
'key' => 'field_61b0c7f2a3e91',
|
||||
'label' => 'Start Time',
|
||||
'name' => 'start_time',
|
||||
'type' => 'time_picker',
|
||||
'instructions' => '',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '20',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'display_format' => 'g:i a',
|
||||
'return_format' => 'H:i:s',
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_end_date',
|
||||
'label' => 'End Date',
|
||||
'name' => 'end_date',
|
||||
'type' => 'date_picker',
|
||||
'instructions' => '',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '25',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'display_format' => 'F j, Y',
|
||||
'return_format' => 'Y-m-d',
|
||||
'first_day' => 1,
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_end_time',
|
||||
'label' => 'End Time',
|
||||
'name' => 'end_time',
|
||||
'type' => 'time_picker',
|
||||
'instructions' => '',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '20',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'display_format' => 'g:i a',
|
||||
'return_format' => 'H:i:s',
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_all_day',
|
||||
'label' => 'All Day',
|
||||
'name' => 'all_day',
|
||||
'type' => 'true_false',
|
||||
'instructions' => '',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '10',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'default_value' => 0,
|
||||
'ui' => 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'key' => 'field_61b0c6cba3e83',
|
||||
'label' => 'Online/In-Person',
|
||||
@@ -126,6 +293,22 @@ function events_register_acf_fields() {
|
||||
'rows' => '',
|
||||
'new_lines' => '',
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_contact_email',
|
||||
'label' => 'Contact Email',
|
||||
'name' => 'event_contact_email',
|
||||
'type' => 'email',
|
||||
'instructions' => '',
|
||||
'required' => 0,
|
||||
'conditional_logic' => 0,
|
||||
'wrapper' => array(
|
||||
'width' => '',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
),
|
||||
'default_value' => '',
|
||||
'placeholder' => '',
|
||||
),
|
||||
),
|
||||
'location' => array(
|
||||
array(
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
/**
|
||||
* Pure calendar layout helpers.
|
||||
*
|
||||
* @package AA_Events
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the complete week grid for one month.
|
||||
*
|
||||
* @param mixed $year Four-digit year.
|
||||
* @param mixed $month Month number.
|
||||
* @param mixed $start_of_week WordPress weekday number (0 Sunday through 6 Saturday).
|
||||
* @return array
|
||||
* @throws InvalidArgumentException For invalid arguments.
|
||||
*/
|
||||
function aa_events_calendar_month( $year, $month, $start_of_week ) {
|
||||
if ( ! is_int( $year ) || ! is_int( $month ) || ! is_int( $start_of_week ) ) {
|
||||
throw new InvalidArgumentException( 'Calendar year, month, and start of week must be integers.' );
|
||||
}
|
||||
if ( $year < 1 || $year > 9999 || $month < 1 || $month > 12 || $start_of_week < 0 || $start_of_week > 6 ) {
|
||||
throw new InvalidArgumentException( 'Invalid calendar year, month, or start of week.' );
|
||||
}
|
||||
|
||||
$month_value = sprintf( '%04d-%02d-01', $year, $month );
|
||||
$month_start = DateTimeImmutable::createFromFormat( '!Y-m-d', $month_value, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
if ( false === $month_start || ( is_array( $errors ) && ( $errors['warning_count'] || $errors['error_count'] ) ) || $month_start->format( 'Y-m-d' ) !== $month_value ) {
|
||||
throw new InvalidArgumentException( 'Invalid calendar month.' );
|
||||
}
|
||||
$month_end = $month_start->modify( 'last day of this month' );
|
||||
$leading_days = ( (int) $month_start->format( 'w' ) - $start_of_week + 7 ) % 7;
|
||||
$grid_start = $month_start->modify( '-' . $leading_days . ' days' );
|
||||
$trailing_days = ( $start_of_week + 6 - (int) $month_end->format( 'w' ) + 7 ) % 7;
|
||||
$grid_end = $month_end->modify( '+' . $trailing_days . ' days' );
|
||||
$weeks = array();
|
||||
$week_start = $grid_start;
|
||||
$week_index = 0;
|
||||
while ( $week_start <= $grid_end ) {
|
||||
$days = array();
|
||||
for ( $day = 0; $day < 7; ++$day ) {
|
||||
$days[] = $week_start->modify( '+' . $day . ' days' );
|
||||
}
|
||||
$weeks[] = array(
|
||||
'index' => $week_index,
|
||||
'key' => $week_start->format( 'Y-m-d' ),
|
||||
'start' => $week_start,
|
||||
'end' => $week_start->modify( '+6 days' ),
|
||||
'days' => $days,
|
||||
);
|
||||
$week_start = $week_start->modify( '+7 days' );
|
||||
++$week_index;
|
||||
}
|
||||
|
||||
return compact( 'month_start', 'month_end', 'grid_start', 'grid_end', 'start_of_week', 'weeks' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an occurrence overlaps an inclusive date range.
|
||||
*
|
||||
* @param array $occurrence Occurrence metadata.
|
||||
* @param DateTimeImmutable $start Range start.
|
||||
* @param DateTimeImmutable $end Range end.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_occurrence_intersects_range( array $occurrence, DateTimeImmutable $start, DateTimeImmutable $end ) {
|
||||
$parse_date = function ( $value ) {
|
||||
if ( ! is_string( $value ) ) {
|
||||
return null;
|
||||
}
|
||||
$date = DateTimeImmutable::createFromFormat( '!Y-m-d', $value, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
return false !== $date && ( ! is_array( $errors ) || ( ! $errors['warning_count'] && ! $errors['error_count'] ) ) && $date->format( 'Y-m-d' ) === $value ? $date : null;
|
||||
};
|
||||
$occurrence_start = $parse_date( $occurrence['start_date'] ?? null );
|
||||
$occurrence_end = $parse_date( $occurrence['end_date'] ?? ( $occurrence['start_date'] ?? null ) );
|
||||
return null !== $occurrence_start && null !== $occurrence_end && $occurrence_end >= $occurrence_start && $end >= $start && $occurrence_start <= $end && $occurrence_end >= $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clip an occurrence to a month and split it at week boundaries.
|
||||
*
|
||||
* @param array $occurrence Occurrence metadata.
|
||||
* @param array $month Calendar month shape.
|
||||
* @return array
|
||||
*/
|
||||
function aa_events_calendar_segments( array $occurrence, array $month ) {
|
||||
if ( ! isset( $month['month_start'], $month['month_end'], $month['weeks'] ) || ! $month['month_start'] instanceof DateTimeImmutable || ! $month['month_end'] instanceof DateTimeImmutable || ! aa_events_occurrence_intersects_range( $occurrence, $month['month_start'], $month['month_end'] ) ) {
|
||||
return array();
|
||||
}
|
||||
$parse_date = function ( $value ) {
|
||||
if ( ! is_string( $value ) ) {
|
||||
return null;
|
||||
}
|
||||
$date = DateTimeImmutable::createFromFormat( '!Y-m-d', $value, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
return false !== $date && ( ! is_array( $errors ) || ( ! $errors['warning_count'] && ! $errors['error_count'] ) ) && $date->format( 'Y-m-d' ) === $value ? $date : null;
|
||||
};
|
||||
$true_start = $parse_date( $occurrence['start_date'] );
|
||||
$true_end = $parse_date( $occurrence['end_date'] ?? $occurrence['start_date'] );
|
||||
$visible_start = max( $true_start, $month['month_start'] );
|
||||
$visible_end = min( $true_end, $month['month_end'] );
|
||||
$segments = array();
|
||||
|
||||
foreach ( $month['weeks'] as $week_index => $week ) {
|
||||
if ( $visible_start > $week['end'] || $visible_end < $week['start'] ) {
|
||||
continue;
|
||||
}
|
||||
$segment_start = max( $visible_start, $week['start'] );
|
||||
$segment_end = min( $visible_end, $week['end'] );
|
||||
$segment = $occurrence;
|
||||
$segment['week_index'] = (int) $week_index;
|
||||
$segment['week_key'] = $week['key'] ?? $week['start']->format( 'Y-m-d' );
|
||||
$segment['segment_start'] = $segment_start;
|
||||
$segment['segment_end'] = $segment_end;
|
||||
$segment['start_column'] = (int) $week['start']->diff( $segment_start )->days;
|
||||
$segment['span'] = (int) $segment_start->diff( $segment_end )->days + 1;
|
||||
$segment['continues_before'] = $true_start < $segment_start;
|
||||
$segment['continues_after'] = $true_end > $segment_end;
|
||||
$segments[] = $segment;
|
||||
}
|
||||
return $segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort segments and assign the first non-overlapping lane.
|
||||
*
|
||||
* @param array $segments Week segments.
|
||||
* @return array
|
||||
*/
|
||||
function aa_events_assign_segment_lanes( array $segments ) {
|
||||
$decorated = array();
|
||||
foreach ( $segments as $index => $segment ) {
|
||||
$decorated[] = array(
|
||||
'segment' => $segment,
|
||||
'order' => $index,
|
||||
);
|
||||
}
|
||||
usort(
|
||||
$decorated,
|
||||
function ( $left, $right ) {
|
||||
$comparison = (int) ( $left['segment']['start_column'] ?? 0 ) <=> (int) ( $right['segment']['start_column'] ?? 0 );
|
||||
if ( 0 === $comparison ) {
|
||||
$comparison = (int) ( $right['segment']['span'] ?? 0 ) <=> (int) ( $left['segment']['span'] ?? 0 );
|
||||
}
|
||||
if ( 0 === $comparison ) {
|
||||
$comparison = (int) ( $left['segment']['post_id'] ?? 0 ) <=> (int) ( $right['segment']['post_id'] ?? 0 );
|
||||
}
|
||||
return 0 === $comparison ? $left['order'] <=> $right['order'] : $comparison;
|
||||
}
|
||||
);
|
||||
|
||||
$lane_ends = array();
|
||||
$segments = array();
|
||||
foreach ( $decorated as $item ) {
|
||||
$segment = $item['segment'];
|
||||
$start = (int) ( $segment['start_column'] ?? 0 );
|
||||
$end = $start + max( 1, (int) ( $segment['span'] ?? 1 ) ) - 1;
|
||||
$lane = 0;
|
||||
while ( isset( $lane_ends[ $lane ] ) && $lane_ends[ $lane ] >= $start ) {
|
||||
++$lane;
|
||||
}
|
||||
$segment['lane'] = $lane;
|
||||
$lane_ends[ $lane ] = $end;
|
||||
$segments[] = $segment;
|
||||
}
|
||||
return $segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group all occurrence segments into the month's complete week list.
|
||||
*
|
||||
* @param array $occurrences Occurrences to lay out.
|
||||
* @param array $month Calendar month shape.
|
||||
* @return array
|
||||
*/
|
||||
function aa_events_group_calendar_weeks( array $occurrences, array $month ) {
|
||||
$weeks = array();
|
||||
foreach ( $month['weeks'] ?? array() as $index => $week ) {
|
||||
$week['segments'] = array();
|
||||
$week['lane_count'] = 0;
|
||||
$weeks[ $index ] = $week;
|
||||
}
|
||||
foreach ( $occurrences as $occurrence ) {
|
||||
if ( ! is_array( $occurrence ) ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( aa_events_calendar_segments( $occurrence, $month ) as $segment ) {
|
||||
if ( isset( $weeks[ $segment['week_index'] ] ) ) {
|
||||
$weeks[ $segment['week_index'] ]['segments'][] = $segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $weeks as &$week ) {
|
||||
$week['segments'] = aa_events_assign_segment_lanes( $week['segments'] );
|
||||
if ( $week['segments'] ) {
|
||||
$week['lane_count'] = max( array_column( $week['segments'], 'lane' ) ) + 1;
|
||||
}
|
||||
}
|
||||
unset( $week );
|
||||
return array_values( $weeks );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort calendar occurrences for the mobile agenda.
|
||||
*
|
||||
* @param array $occurrences Enriched normalized occurrences.
|
||||
* @return array
|
||||
*/
|
||||
function aa_events_sort_calendar_occurrences( array $occurrences ) {
|
||||
$decorated = array();
|
||||
foreach ( $occurrences as $index => $occurrence ) {
|
||||
if ( ! is_array( $occurrence ) ) {
|
||||
continue;
|
||||
}
|
||||
$decorated[] = array(
|
||||
'occurrence' => $occurrence,
|
||||
'index' => $index,
|
||||
);
|
||||
}
|
||||
|
||||
usort(
|
||||
$decorated,
|
||||
function ( $left, $right ) {
|
||||
$left_occurrence = $left['occurrence'];
|
||||
$right_occurrence = $right['occurrence'];
|
||||
$comparison = aa_events_occurrence_start( $left_occurrence )->getTimestamp() <=> aa_events_occurrence_start( $right_occurrence )->getTimestamp();
|
||||
if ( 0 === $comparison ) {
|
||||
$comparison = strcasecmp( (string) ( $left_occurrence['title'] ?? '' ), (string) ( $right_occurrence['title'] ?? '' ) );
|
||||
}
|
||||
if ( 0 === $comparison ) {
|
||||
$comparison = (int) ( $left_occurrence['post_id'] ?? 0 ) <=> (int) ( $right_occurrence['post_id'] ?? 0 );
|
||||
}
|
||||
foreach ( array( 'end_date', 'end_time' ) as $key ) {
|
||||
if ( 0 !== $comparison ) {
|
||||
break;
|
||||
}
|
||||
$comparison = strcmp( (string) ( $left_occurrence[ $key ] ?? '' ), (string) ( $right_occurrence[ $key ] ?? '' ) );
|
||||
}
|
||||
return 0 === $comparison ? $left['index'] <=> $right['index'] : $comparison;
|
||||
}
|
||||
);
|
||||
|
||||
return array_column( $decorated, 'occurrence' );
|
||||
}
|
||||
@@ -50,6 +50,8 @@ final class AA_Events {
|
||||
* Include required core files.
|
||||
*/
|
||||
public function includes() {
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/occurrences.php';
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/calendar-layout.php';
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/post-types.php';
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/acf-fields.php';
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/template-loader.php';
|
||||
@@ -61,8 +63,10 @@ final class AA_Events {
|
||||
private function init_hooks() {
|
||||
add_action( 'init', array( $this, 'load_plugin_textdomain' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
// Ensure the active theme supports thumbnails for the event post type.
|
||||
add_action( 'after_setup_theme', array( $this, 'ensure_post_thumbnails_support' ) );
|
||||
add_filter( 'query_vars', array( $this, 'register_calendar_query_vars' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +92,31 @@ final class AA_Events {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets for event editor UX fixes.
|
||||
*
|
||||
* @param string $hook_suffix Current admin page hook.
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_admin_assets( $hook_suffix ) {
|
||||
if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || 'event' !== $screen->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'aa-events-admin',
|
||||
AA_EVENTS_PLUGIN_URL . 'assets/js/aa-events-admin.js',
|
||||
array( 'jquery' ),
|
||||
AA_EVENTS_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Localisation files.
|
||||
*/
|
||||
@@ -125,4 +154,17 @@ final class AA_Events {
|
||||
}
|
||||
// If theme support is boolean true (all types), nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow custom AA Events calendar query vars to persist through canonical redirects.
|
||||
*
|
||||
* @param array $vars Public query vars.
|
||||
* @return array
|
||||
*/
|
||||
public function register_calendar_query_vars( $vars ) {
|
||||
$vars[] = 'aa_month';
|
||||
$vars[] = 'aa_year';
|
||||
|
||||
return $vars;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,755 @@
|
||||
<?php
|
||||
/**
|
||||
* Event occurrence normalization, validation, and display helpers.
|
||||
*
|
||||
* @package AA_Events
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safely convert scalar or stringable field input to a trimmed string.
|
||||
*
|
||||
* @param mixed $value Raw field value.
|
||||
* @return string|null
|
||||
*/
|
||||
function aa_events_occurrence_string( $value ) {
|
||||
if ( is_scalar( $value ) ) {
|
||||
return trim( (string) $value );
|
||||
}
|
||||
if ( is_object( $value ) && method_exists( $value, '__toString' ) ) {
|
||||
try {
|
||||
return trim( (string) $value );
|
||||
} catch ( Throwable $error ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly parse and normalize a date or time value.
|
||||
*
|
||||
* @param mixed $value Value to parse.
|
||||
* @param string $format Input format.
|
||||
* @param string $output Output format.
|
||||
* @return string|null
|
||||
*/
|
||||
function aa_events_parse_occurrence_value( $value, $format, $output ) {
|
||||
$value = aa_events_occurrence_string( $value );
|
||||
if ( null === $value || '' === $value ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat( '!' . $format, $value, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
if ( false === $date || ( is_array( $errors ) && ( $errors['warning_count'] || $errors['error_count'] ) ) || $date->format( $format ) !== $value ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date->format( $output );
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an ACF-style boolean without treating the string "false" as true.
|
||||
*
|
||||
* @param mixed $value Raw value.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_occurrence_boolean( $value ) {
|
||||
$value = aa_events_occurrence_string( $value );
|
||||
return null !== $value && in_array( strtolower( $value ), array( '1', 'true', 'yes', 'on' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a time with or without seconds.
|
||||
*
|
||||
* @param mixed $value Raw time.
|
||||
* @return string|null
|
||||
*/
|
||||
function aa_events_parse_occurrence_time( $value ) {
|
||||
$time = aa_events_parse_occurrence_value( $value, 'H:i:s', 'H:i:s' );
|
||||
return null === $time ? aa_events_parse_occurrence_value( $value, 'H:i', 'H:i:s' ) : $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm a normalized time exists on its local calendar date.
|
||||
*
|
||||
* PHP deterministically selects an offset for ambiguous fall-back wall times;
|
||||
* exact round-trip checking rejects only nonexistent spring-forward wall times.
|
||||
*
|
||||
* @param string $date Date in Y-m-d format.
|
||||
* @param string $time Time in H:i:s format.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_occurrence_local_time_is_valid( $date, $time ) {
|
||||
$value = $date . ' ' . $time;
|
||||
$parsed = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $value, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
return false !== $parsed && ( ! is_array( $errors ) || ( ! $errors['warning_count'] && ! $errors['error_count'] ) ) && $parsed->format( 'Y-m-d H:i:s' ) === $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate already-parsed occurrence values without calling public validation.
|
||||
*
|
||||
* @param string $start_date Start date.
|
||||
* @param string $start_time Optional start time.
|
||||
* @param string $end_date Effective end date.
|
||||
* @param string $end_time Optional end time.
|
||||
* @param bool $all_day Whether times are ignored.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_occurrence_range_is_valid( $start_date, $start_time, $end_date, $end_time, $all_day ) {
|
||||
if ( $end_date < $start_date ) {
|
||||
return false;
|
||||
}
|
||||
if ( $all_day ) {
|
||||
return true;
|
||||
}
|
||||
if ( $start_time && ! aa_events_occurrence_local_time_is_valid( $start_date, $start_time ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $end_time && ! aa_events_occurrence_local_time_is_valid( $end_date, $end_time ) ) {
|
||||
return false;
|
||||
}
|
||||
return $start_date !== $end_date || ! $start_time || ! $end_time || $end_time >= $start_time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a single occurrence row.
|
||||
*
|
||||
* The public shape records provenance without temporary keys. start_inferred is
|
||||
* true when the start came from a legacy combined/separate source rather than
|
||||
* a current repeater row. end_inferred is true when the effective end was
|
||||
* defaulted because no explicit end was supplied, and for all legacy sources.
|
||||
*
|
||||
* @param array $row Raw occurrence fields.
|
||||
* @param string $source Source field format.
|
||||
* @param int $post_id Event post ID.
|
||||
* @return array|null
|
||||
*/
|
||||
function aa_events_normalize_occurrence_row( array $row, $source = 'event_dates', $post_id = 0 ) {
|
||||
$source = in_array( $source, array( 'event_dates', 'event_datetime', 'legacy_separate' ), true ) ? $source : 'event_dates';
|
||||
$start_inferred = 'event_dates' !== $source;
|
||||
$start_date = aa_events_parse_occurrence_value( $row['start_date'] ?? $row['date'] ?? '', 'Y-m-d', 'Y-m-d' );
|
||||
if ( null === $start_date ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$end_input = aa_events_occurrence_string( $row['end_date'] ?? '' );
|
||||
if ( null === $end_input ) {
|
||||
return null;
|
||||
}
|
||||
$end_date = '' === $end_input ? $start_date : aa_events_parse_occurrence_value( $end_input, 'Y-m-d', 'Y-m-d' );
|
||||
if ( null === $end_date ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$all_day = aa_events_occurrence_boolean( $row['all_day'] ?? false );
|
||||
$end_time_input = aa_events_occurrence_string( $row['end_time'] ?? '' );
|
||||
$end_inferred = $start_inferred || ( '' === $end_input && ( null === $end_time_input || '' === $end_time_input ) );
|
||||
$start_time = '';
|
||||
$end_time = '';
|
||||
if ( ! $all_day ) {
|
||||
if ( ! empty( $row['start_time'] ) ) {
|
||||
$start_time = aa_events_parse_occurrence_time( $row['start_time'] );
|
||||
if ( null === $start_time ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if ( ! empty( $row['end_time'] ) ) {
|
||||
$end_time = aa_events_parse_occurrence_time( $row['end_time'] );
|
||||
if ( null === $end_time ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! aa_events_occurrence_range_is_valid( $start_date, $start_time, $end_date, $end_time, $all_day ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'post_id' => (int) $post_id,
|
||||
'start_date' => $start_date,
|
||||
'start_time' => $start_time ? $start_time : '',
|
||||
'end_date' => $end_date,
|
||||
'end_time' => $end_time ? $end_time : '',
|
||||
'all_day' => (bool) $all_day,
|
||||
'source' => $source,
|
||||
'start_inferred' => $start_inferred,
|
||||
'end_inferred' => $end_inferred,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all occurrences for an event, preferring the current repeater format.
|
||||
*
|
||||
* @param int $post_id Event post ID.
|
||||
* @return array
|
||||
*/
|
||||
function aa_events_get_occurrences( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
$rows = get_field( 'event_dates', $post_id );
|
||||
$occurrences = array();
|
||||
|
||||
if ( is_array( $rows ) && $rows ) {
|
||||
foreach ( $rows as $row ) {
|
||||
if ( is_array( $row ) ) {
|
||||
$occurrence = aa_events_normalize_occurrence_row( $row, 'event_dates', $post_id );
|
||||
if ( null !== $occurrence ) {
|
||||
$occurrences[] = $occurrence;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$legacy = aa_events_occurrence_string( get_field( 'event_datetime', $post_id ) );
|
||||
if ( null !== $legacy && '' !== $legacy ) {
|
||||
$date = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $legacy, wp_timezone() );
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
if ( false !== $date && ( ! is_array( $errors ) || ( ! $errors['warning_count'] && ! $errors['error_count'] ) ) && $date->format( 'Y-m-d H:i:s' ) === $legacy ) {
|
||||
$occurrences[] = aa_events_normalize_occurrence_row(
|
||||
array(
|
||||
'date' => $date->format( 'Y-m-d' ),
|
||||
'start_time' => $date->format( 'H:i:s' ),
|
||||
),
|
||||
'event_datetime',
|
||||
$post_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$date = get_field( 'date', $post_id );
|
||||
if ( null !== aa_events_occurrence_string( $date ) && '' !== aa_events_occurrence_string( $date ) ) {
|
||||
$occurrence = aa_events_normalize_occurrence_row(
|
||||
array(
|
||||
'date' => $date,
|
||||
'start_time' => get_field( 'start_time', $post_id ),
|
||||
),
|
||||
'legacy_separate',
|
||||
$post_id
|
||||
);
|
||||
if ( null !== $occurrence ) {
|
||||
$occurrences[] = $occurrence;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$decorated = array();
|
||||
foreach ( $occurrences as $index => $occurrence ) {
|
||||
$decorated[] = array(
|
||||
'occurrence' => $occurrence,
|
||||
'index' => $index,
|
||||
);
|
||||
}
|
||||
|
||||
usort(
|
||||
$decorated,
|
||||
function ( $left, $right ) {
|
||||
$left_occurrence = $left['occurrence'];
|
||||
$right_occurrence = $right['occurrence'];
|
||||
$comparison = aa_events_occurrence_start( $left_occurrence )->getTimestamp() <=> aa_events_occurrence_start( $right_occurrence )->getTimestamp();
|
||||
if ( 0 !== $comparison ) {
|
||||
return $comparison;
|
||||
}
|
||||
foreach ( array( 'end_date', 'end_time' ) as $key ) {
|
||||
$comparison = strcmp( $left_occurrence[ $key ], $right_occurrence[ $key ] );
|
||||
if ( 0 !== $comparison ) {
|
||||
return $comparison;
|
||||
}
|
||||
}
|
||||
$comparison = (int) $left_occurrence['all_day'] <=> (int) $right_occurrence['all_day'];
|
||||
if ( 0 !== $comparison ) {
|
||||
return $comparison;
|
||||
}
|
||||
$comparison = strcmp( $left_occurrence['source'], $right_occurrence['source'] );
|
||||
return 0 !== $comparison ? $comparison : $left['index'] <=> $right['index'];
|
||||
}
|
||||
);
|
||||
|
||||
return array_column( $decorated, 'occurrence' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event dates in the legacy theme-helper shape.
|
||||
*
|
||||
* @deprecated Use aa_events_get_occurrences() instead.
|
||||
*
|
||||
* @param int $post_id Event post ID.
|
||||
* @return array<int,array{date:string,time:string,end_date:string,end_time:string,all_day:bool}>
|
||||
*/
|
||||
function events_get_all_event_dates( $post_id ) {
|
||||
return array_map(
|
||||
function ( $occurrence ) {
|
||||
return array(
|
||||
'date' => $occurrence['start_date'],
|
||||
'time' => $occurrence['start_time'],
|
||||
'end_date' => $occurrence['end_date'],
|
||||
'end_time' => $occurrence['end_time'],
|
||||
'all_day' => $occurrence['all_day'],
|
||||
);
|
||||
},
|
||||
aa_events_get_occurrences( $post_id )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a raw or normalized occurrence row.
|
||||
*
|
||||
* @param array $row Occurrence fields.
|
||||
* @return string Empty when valid, otherwise an error message.
|
||||
*/
|
||||
function aa_events_validate_occurrence_row( array $row ) {
|
||||
$start = aa_events_parse_occurrence_value( $row['start_date'] ?? $row['date'] ?? '', 'Y-m-d', 'Y-m-d' );
|
||||
if ( null === $start ) {
|
||||
return __( 'Start date is required and must be valid.', 'aa-events' );
|
||||
}
|
||||
$end_input = aa_events_occurrence_string( $row['end_date'] ?? '' );
|
||||
if ( null === $end_input ) {
|
||||
return __( 'End date must be valid.', 'aa-events' );
|
||||
}
|
||||
$end = '' === $end_input ? $start : aa_events_parse_occurrence_value( $end_input, 'Y-m-d', 'Y-m-d' );
|
||||
if ( null === $end ) {
|
||||
return __( 'End date must be valid.', 'aa-events' );
|
||||
}
|
||||
if ( $end < $start ) {
|
||||
return __( 'End date cannot be earlier than start date.', 'aa-events' );
|
||||
}
|
||||
if ( aa_events_occurrence_boolean( $row['all_day'] ?? false ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$start_time = empty( $row['start_time'] ) ? '' : aa_events_parse_occurrence_time( $row['start_time'] );
|
||||
$end_time = empty( $row['end_time'] ) ? '' : aa_events_parse_occurrence_time( $row['end_time'] );
|
||||
if ( ! empty( $row['start_time'] ) && null === $start_time ) {
|
||||
return __( 'Start time must be valid.', 'aa-events' );
|
||||
}
|
||||
if ( ! empty( $row['end_time'] ) && null === $end_time ) {
|
||||
return __( 'End time must be valid.', 'aa-events' );
|
||||
}
|
||||
if ( $start_time && ! aa_events_occurrence_local_time_is_valid( $start, $start_time ) ) {
|
||||
return __( 'Start time must be valid.', 'aa-events' );
|
||||
}
|
||||
if ( $end_time && ! aa_events_occurrence_local_time_is_valid( $end, $end_time ) ) {
|
||||
return __( 'End time must be valid.', 'aa-events' );
|
||||
}
|
||||
if ( $start === $end && $start_time && $end_time && $end_time < $start_time ) {
|
||||
return __( 'End time cannot be earlier than start time on the same date.', 'aa-events' );
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an occurrence start value.
|
||||
*
|
||||
* @param array $occurrence Normalized occurrence.
|
||||
* @return DateTimeImmutable
|
||||
*/
|
||||
function aa_events_occurrence_start( array $occurrence ) {
|
||||
$time = empty( $occurrence['all_day'] ) && ! empty( $occurrence['start_time'] ) ? $occurrence['start_time'] : '00:00:00';
|
||||
return new DateTimeImmutable( $occurrence['start_date'] . ' ' . $time, wp_timezone() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an occurrence end value.
|
||||
*
|
||||
* @param array $occurrence Normalized occurrence.
|
||||
* @return DateTimeImmutable
|
||||
*/
|
||||
function aa_events_occurrence_end( array $occurrence ) {
|
||||
$end_date = $occurrence['end_date'] ? $occurrence['end_date'] : $occurrence['start_date'];
|
||||
if ( ! empty( $occurrence['all_day'] ) ) {
|
||||
$time = '00:00:00';
|
||||
} elseif ( ! empty( $occurrence['end_time'] ) ) {
|
||||
$time = $occurrence['end_time'];
|
||||
} elseif ( $end_date === $occurrence['start_date'] && ! empty( $occurrence['start_time'] ) ) {
|
||||
$time = $occurrence['start_time'];
|
||||
} else {
|
||||
$time = '00:00:00';
|
||||
}
|
||||
return new DateTimeImmutable( $end_date . ' ' . $time, wp_timezone() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a normalized occurrence for display.
|
||||
*
|
||||
* @param array $occurrence Normalized occurrence.
|
||||
* @return string
|
||||
*/
|
||||
function aa_events_format_occurrence( array $occurrence ) {
|
||||
$timezone = wp_timezone();
|
||||
$start = aa_events_occurrence_start( $occurrence );
|
||||
$end = aa_events_occurrence_end( $occurrence );
|
||||
$same_day = $occurrence['start_date'] === $occurrence['end_date'];
|
||||
|
||||
if ( ! empty( $occurrence['all_day'] ) ) {
|
||||
if ( $same_day ) {
|
||||
$label = wp_date( 'F j, Y', $start->getTimestamp(), $timezone );
|
||||
} elseif ( $start->format( 'Y-m' ) === $end->format( 'Y-m' ) ) {
|
||||
$label = wp_date( 'F j', $start->getTimestamp(), $timezone ) . '–' . wp_date( 'j, Y', $end->getTimestamp(), $timezone );
|
||||
} elseif ( $start->format( 'Y' ) === $end->format( 'Y' ) ) {
|
||||
$label = wp_date( 'F j', $start->getTimestamp(), $timezone ) . '–' . wp_date( 'F j, Y', $end->getTimestamp(), $timezone );
|
||||
} else {
|
||||
$label = wp_date( 'F j, Y', $start->getTimestamp(), $timezone ) . '–' . wp_date( 'F j, Y', $end->getTimestamp(), $timezone );
|
||||
}
|
||||
return $label . ' — ' . __( 'All day', 'aa-events' );
|
||||
}
|
||||
|
||||
$start_date = wp_date( 'F j, Y', $start->getTimestamp(), $timezone );
|
||||
$start_time = empty( $occurrence['start_time'] ) ? '' : wp_date( 'g:i A', $start->getTimestamp(), $timezone );
|
||||
$end_time = empty( $occurrence['end_time'] ) ? '' : wp_date( 'g:i A', $end->getTimestamp(), $timezone );
|
||||
if ( $same_day ) {
|
||||
if ( $start_time && $end_time ) {
|
||||
return $start_date . ', ' . $start_time . '–' . $end_time;
|
||||
}
|
||||
if ( $end_time ) {
|
||||
/* translators: 1: localized occurrence date, 2: localized end time. */
|
||||
return sprintf( __( '%1$s, until %2$s', 'aa-events' ), $start_date, $end_time );
|
||||
}
|
||||
return $start_date . ( $start_time ? ', ' . $start_time : '' );
|
||||
}
|
||||
$start_label = $start_date . ( $start_time ? ', ' . $start_time : '' );
|
||||
$end_label = wp_date( 'F j, Y', $end->getTimestamp(), $timezone ) . ( $end_time ? ', ' . $end_time : '' );
|
||||
return $start_label . '–' . $end_label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render safe, accessible time markup for one normalized occurrence.
|
||||
*
|
||||
* Points return one visible <time>. Ranges return an aria-hidden visible
|
||||
* formatted label plus a screen-reader-only, localized pair of endpoint
|
||||
* <time> elements. Each endpoint is date-only unless that endpoint has an
|
||||
* explicit time; all returned dynamic text and attributes are escaped.
|
||||
*
|
||||
* @param array $occurrence Normalized occurrence from aa_events_get_occurrences().
|
||||
* @return string Escaped HTML safe to print directly.
|
||||
*/
|
||||
function aa_events_occurrence_time_markup( array $occurrence ) {
|
||||
$start = aa_events_occurrence_start( $occurrence );
|
||||
$end = aa_events_occurrence_end( $occurrence );
|
||||
$all_day = ! empty( $occurrence['all_day'] );
|
||||
$has_start_time = ! $all_day && ! empty( $occurrence['start_time'] );
|
||||
$has_end_time = ! $all_day && ! empty( $occurrence['end_time'] );
|
||||
$is_range = $occurrence['start_date'] !== $occurrence['end_date'] || $has_end_time;
|
||||
$visible = esc_html( aa_events_format_occurrence( $occurrence ) );
|
||||
$start_value = $has_start_time ? $start->format( DATE_W3C ) : $occurrence['start_date'];
|
||||
|
||||
if ( ! $is_range ) {
|
||||
return '<time datetime="' . esc_attr( $start_value ) . '">' . $visible . '</time>';
|
||||
}
|
||||
|
||||
$end_value = $has_end_time ? $end->format( DATE_W3C ) : $occurrence['end_date'];
|
||||
$start_label = wp_date( $has_start_time ? 'F j, Y, g:i A' : 'F j, Y', $start->getTimestamp(), wp_timezone() );
|
||||
$end_label = wp_date( $has_end_time ? 'F j, Y, g:i A' : 'F j, Y', $end->getTimestamp(), wp_timezone() );
|
||||
|
||||
return '<span aria-hidden="true">' . $visible . '</span>'
|
||||
. '<span class="screen-reader-text">' . esc_html( __( 'Starts:', 'aa-events' ) )
|
||||
. ' <time datetime="' . esc_attr( $start_value ) . '">' . esc_html( $start_label ) . '</time>. '
|
||||
. esc_html( __( 'Ends:', 'aa-events' ) )
|
||||
. ' <time datetime="' . esc_attr( $end_value ) . '">' . esc_html( $end_label ) . '</time>.</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the first parseable legacy occurrence into the repeater.
|
||||
*
|
||||
* @param int $post_id Event post ID.
|
||||
* @return bool Whether a repeater row was written.
|
||||
*/
|
||||
function aa_events_migrate_legacy_occurrence( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
$rows = get_field( 'event_dates', $post_id );
|
||||
if ( is_array( $rows ) && $rows ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$occurrences = aa_events_get_occurrences( $post_id );
|
||||
if ( ! isset( $occurrences[0] ) || ! in_array( $occurrences[0]['source'], array( 'event_datetime', 'legacy_separate' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$occurrence = $occurrences[0];
|
||||
$row = array(
|
||||
'date' => $occurrence['start_date'],
|
||||
'start_time' => $occurrence['start_time'],
|
||||
'end_date' => '',
|
||||
'end_time' => '',
|
||||
'all_day' => 0,
|
||||
);
|
||||
|
||||
return (bool) update_field( 'event_dates', array( $row ), $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate legacy occurrence data after ACF has saved an event.
|
||||
*
|
||||
* @param int $post_id Saved post ID.
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_migrate_legacy_on_save( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
if ( ! $post_id || wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || 'event' !== get_post_type( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
aa_events_migrate_legacy_occurrence( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain the canonical earliest occurrence value used for event sorting.
|
||||
*
|
||||
* @param int $post_id Saved post ID.
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_sync_sort_start_on_save( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
if ( ! $post_id || wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || 'event' !== get_post_type( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$occurrences = aa_events_get_occurrences( $post_id );
|
||||
if ( ! isset( $occurrences[0] ) ) {
|
||||
delete_post_meta( $post_id, '_aa_events_sort_start' );
|
||||
delete_post_meta( $post_id, '_aa_events_sort_end' );
|
||||
update_post_meta( $post_id, '_aa_events_occurrence_index_version', AA_EVENTS_VERSION );
|
||||
return;
|
||||
}
|
||||
|
||||
$occurrence = $occurrences[0];
|
||||
$sort_start = $occurrence['start_date'] . ' ' . ( $occurrence['start_time'] ? $occurrence['start_time'] : '00:00:00' );
|
||||
$sort_end = aa_events_occurrence_end( $occurrence );
|
||||
foreach ( $occurrences as $candidate ) {
|
||||
$candidate_end = aa_events_occurrence_end( $candidate );
|
||||
if ( $candidate_end > $sort_end ) {
|
||||
$sort_end = $candidate_end;
|
||||
}
|
||||
}
|
||||
update_post_meta( $post_id, '_aa_events_sort_start', $sort_start );
|
||||
update_post_meta( $post_id, '_aa_events_sort_end', $sort_end->format( 'Y-m-d H:i:s' ) );
|
||||
update_post_meta( $post_id, '_aa_events_occurrence_index_version', AA_EVENTS_VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule one bounded occurrence-index backfill batch when none is pending.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_schedule_occurrence_index_backfill() {
|
||||
if ( AA_EVENTS_VERSION !== get_option( 'aa_events_occurrence_index_version', '' ) && ! wp_next_scheduled( 'aa_events_occurrence_index_backfill' ) ) {
|
||||
wp_schedule_single_event( time() + 60, 'aa_events_occurrence_index_backfill' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill one bounded batch of published events with missing/outdated indexes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_run_occurrence_index_backfill() {
|
||||
$batch = new WP_Query(
|
||||
array(
|
||||
'post_type' => 'event',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 50,
|
||||
'fields' => 'ids',
|
||||
'no_found_rows' => true,
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_aa_events_occurrence_index_version',
|
||||
'value' => AA_EVENTS_VERSION,
|
||||
'compare' => '!=',
|
||||
),
|
||||
array(
|
||||
'key' => '_aa_events_occurrence_index_version',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
foreach ( $batch->posts as $post_id ) {
|
||||
aa_events_sync_sort_start_on_save( $post_id );
|
||||
}
|
||||
if ( 50 === count( $batch->posts ) ) {
|
||||
aa_events_schedule_occurrence_index_backfill();
|
||||
} else {
|
||||
update_option( 'aa_events_occurrence_index_version', AA_EVENTS_VERSION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a metadata key can affect an event occurrence start.
|
||||
*
|
||||
* @param string $meta_key Metadata key.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_sort_source_meta_key( $meta_key ) {
|
||||
if ( in_array( $meta_key, array( 'event_dates', 'event_datetime', 'date', 'start_time' ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) preg_match( '/^event_dates_[0-9]+_(date|start_time|end_date|end_time|all_day)$/', $meta_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an event for canonical sort synchronization after a metadata mutation.
|
||||
*
|
||||
* ACF and direct WordPress metadata updates can emit several hooks per logical
|
||||
* change. The queue coalesces those writes so fresh values are read once at
|
||||
* request shutdown, after all related row mutations have completed.
|
||||
*
|
||||
* @param mixed $meta_id Metadata ID or IDs.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Metadata key.
|
||||
* @param mixed $meta_value Metadata value.
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_queue_sort_sync_for_meta_change( $meta_id, $object_id, $meta_key, $meta_value ) {
|
||||
unset( $meta_id, $meta_value );
|
||||
$object_id = (int) $object_id;
|
||||
if ( ! aa_events_sort_source_meta_key( (string) $meta_key ) || ! $object_id || 'event' !== get_post_type( $object_id ) || wp_is_post_autosave( $object_id ) || wp_is_post_revision( $object_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $GLOBALS['aa_events_sort_sync_queue'] ) || ! is_array( $GLOBALS['aa_events_sort_sync_queue'] ) ) {
|
||||
$GLOBALS['aa_events_sort_sync_queue'] = array();
|
||||
}
|
||||
$GLOBALS['aa_events_sort_sync_queue'][ $object_id ] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize all canonical event start values queued during this request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function aa_events_run_queued_sort_sync() {
|
||||
$queue = isset( $GLOBALS['aa_events_sort_sync_queue'] ) && is_array( $GLOBALS['aa_events_sort_sync_queue'] ) ? $GLOBALS['aa_events_sort_sync_queue'] : array();
|
||||
$GLOBALS['aa_events_sort_sync_queue'] = array();
|
||||
|
||||
foreach ( array_keys( $queue ) as $post_id ) {
|
||||
if ( function_exists( 'acf_flush_value_cache' ) ) {
|
||||
foreach ( array( 'event_dates', 'event_datetime', 'date', 'start_time' ) as $field_name ) {
|
||||
acf_flush_value_cache( $post_id, $field_name );
|
||||
}
|
||||
}
|
||||
aa_events_sync_sort_start_on_save( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an ACF occurrence repeater belongs to an event edit form.
|
||||
*
|
||||
* @param array $field ACF field definition.
|
||||
* @return bool
|
||||
*/
|
||||
function aa_events_occurrence_validation_applies( $field ) {
|
||||
if ( ! is_array( $field ) || ( isset( $field['type'] ) && 'repeater' !== $field['type'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( 'field_61b0c7f0a3e8f' === ( $field['key'] ?? '' ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( 'event_dates' !== ( $field['name'] ?? '' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_id = function_exists( 'acf_get_form_data' ) ? acf_get_form_data( 'post_id' ) : 0;
|
||||
if ( is_string( $post_id ) && 0 === strpos( $post_id, 'post_' ) ) {
|
||||
$post_id = substr( $post_id, 5 );
|
||||
}
|
||||
|
||||
return (int) $post_id > 0 && 'event' === get_post_type( (int) $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a submitted ACF date picker value for occurrence validation.
|
||||
*
|
||||
* @param mixed $value Submitted date value.
|
||||
* @return mixed
|
||||
*/
|
||||
function aa_events_normalize_submitted_date( $value ) {
|
||||
$string = aa_events_occurrence_string( $value );
|
||||
if ( null === $string || ! preg_match( '/^[0-9]{8}$/', $string ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$normalized = aa_events_parse_occurrence_value( $string, 'Ymd', 'Y-m-d' );
|
||||
return null === $normalized ? $value : $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate submitted ACF repeater rows using stable subfield keys.
|
||||
*
|
||||
* @param mixed $valid Existing validation state.
|
||||
* @param mixed $value Submitted repeater value.
|
||||
* @param array $field ACF field definition.
|
||||
* @param string $input Submitted input name.
|
||||
* @return mixed
|
||||
*/
|
||||
function aa_events_validate_occurrence_repeater( $valid, $value, $field, $input ) {
|
||||
unset( $input );
|
||||
if ( true !== $valid || ! aa_events_occurrence_validation_applies( $field ) ) {
|
||||
return $valid;
|
||||
}
|
||||
if ( '' === $value || null === $value ) {
|
||||
return true;
|
||||
}
|
||||
if ( ! is_array( $value ) ) {
|
||||
return __( 'Occurrences data is malformed.', 'aa-events' );
|
||||
}
|
||||
unset( $value['acfcloneindex'] );
|
||||
|
||||
$key_map = array(
|
||||
'field_61b0c7f1a3e90' => 'date',
|
||||
'field_61b0c7f2a3e91' => 'start_time',
|
||||
'field_aa_events_end_date' => 'end_date',
|
||||
'field_aa_events_end_time' => 'end_time',
|
||||
'field_aa_events_all_day' => 'all_day',
|
||||
);
|
||||
foreach ( $field['sub_fields'] ?? array() as $sub_field ) {
|
||||
if ( is_array( $sub_field ) && ! empty( $sub_field['key'] ) && ! empty( $sub_field['name'] ) ) {
|
||||
$key_map[ $sub_field['key'] ] = $sub_field['name'];
|
||||
}
|
||||
}
|
||||
foreach ( array( 'date', 'start_time', 'end_date', 'end_time', 'all_day' ) as $name ) {
|
||||
$key_map[ $name ] = $name;
|
||||
}
|
||||
foreach ( array_values( $value ) as $index => $submitted_row ) {
|
||||
if ( ! is_array( $submitted_row ) ) {
|
||||
/* translators: %d: one-based occurrence row number. */
|
||||
return sprintf( __( 'Occurrence %d: Occurrence data is malformed.', 'aa-events' ), $index + 1 );
|
||||
}
|
||||
$row = array(
|
||||
'date' => '',
|
||||
'start_time' => '',
|
||||
'end_date' => '',
|
||||
'end_time' => '',
|
||||
'all_day' => '',
|
||||
);
|
||||
foreach ( $key_map as $key => $name ) {
|
||||
if ( array_key_exists( $key, $submitted_row ) ) {
|
||||
$row[ $name ] = $submitted_row[ $key ];
|
||||
}
|
||||
}
|
||||
$row['date'] = aa_events_normalize_submitted_date( $row['date'] );
|
||||
$row['end_date'] = aa_events_normalize_submitted_date( $row['end_date'] );
|
||||
$error = aa_events_validate_occurrence_row( $row );
|
||||
if ( '' !== $error ) {
|
||||
/* translators: 1: one-based occurrence row number, 2: validation error. */
|
||||
return sprintf( __( 'Occurrence %1$d: %2$s', 'aa-events' ), $index + 1, $error );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( function_exists( 'add_action' ) ) {
|
||||
add_action( 'acf/save_post', 'aa_events_migrate_legacy_on_save', 20 );
|
||||
add_action( 'acf/save_post', 'aa_events_sync_sort_start_on_save', 30 );
|
||||
add_action( 'added_post_meta', 'aa_events_queue_sort_sync_for_meta_change', 10, 4 );
|
||||
add_action( 'updated_post_meta', 'aa_events_queue_sort_sync_for_meta_change', 10, 4 );
|
||||
add_action( 'deleted_post_meta', 'aa_events_queue_sort_sync_for_meta_change', 10, 4 );
|
||||
add_action( 'shutdown', 'aa_events_run_queued_sort_sync' );
|
||||
add_action( 'init', 'aa_events_schedule_occurrence_index_backfill' );
|
||||
add_action( 'aa_events_occurrence_index_backfill', 'aa_events_run_occurrence_index_backfill' );
|
||||
}
|
||||
if ( function_exists( 'add_filter' ) ) {
|
||||
add_filter( 'acf/validate_value/key=field_61b0c7f0a3e8f', 'aa_events_validate_occurrence_repeater', 10, 4 );
|
||||
add_filter( 'acf/validate_value/name=event_dates', 'aa_events_validate_occurrence_repeater', 10, 4 );
|
||||
}
|
||||
+146
-19
@@ -146,10 +146,15 @@ add_filter( 'manage_edit-event_columns', 'events_add_admin_columns' );
|
||||
*/
|
||||
function events_display_admin_column_content( $column, $post_id ) {
|
||||
if ( 'event_datetime' === $column ) {
|
||||
$datetime = get_field( 'event_datetime', $post_id );
|
||||
$occurrences = aa_events_get_occurrences( $post_id );
|
||||
if ( ! $occurrences ) {
|
||||
echo esc_html( '—' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $datetime ) {
|
||||
echo esc_html( gmdate( 'F j, Y g:i a', strtotime( $datetime ) ) );
|
||||
echo esc_html( aa_events_format_occurrence( $occurrences[0] ) );
|
||||
if ( count( $occurrences ) > 1 ) {
|
||||
echo ' <span class="aa-event-occurrence-count">+' . esc_html( count( $occurrences ) - 1 ) . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,13 +198,27 @@ function events_get_sortable_meta_map() {
|
||||
* @param WP_Query $query The WordPress query object.
|
||||
*/
|
||||
function events_sort_by_custom_column( $query ) {
|
||||
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||
if ( ! is_admin() || ! $query->is_main_query() || 'event' !== $query->get( 'post_type' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$orderby = $query->get( 'orderby' );
|
||||
$map = events_get_sortable_meta_map();
|
||||
|
||||
// Special handling for the event_datetime column in admin.
|
||||
// Use a custom SQL sort across multiple possible date fields while keeping all posts visible.
|
||||
if ( 'event_datetime' === $orderby ) {
|
||||
$query->set( 'aa_events_date_sort', 1 );
|
||||
$query->set( 'orderby', 'none' );
|
||||
|
||||
$order = strtoupper( (string) $query->get( 'order' ) );
|
||||
if ( 'DESC' !== $order ) {
|
||||
$order = 'ASC';
|
||||
}
|
||||
$query->set( 'order', $order );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $map[ $orderby ] ) ) {
|
||||
$meta_type = strtoupper( $map[ $orderby ] );
|
||||
$query->set( 'meta_key', $orderby );
|
||||
@@ -213,6 +232,115 @@ function events_sort_by_custom_column( $query ) {
|
||||
}
|
||||
add_action( 'pre_get_posts', 'events_sort_by_custom_column' );
|
||||
|
||||
/**
|
||||
* Build a safe SQL expression for supported stored date formats.
|
||||
*
|
||||
* @param string $column Static SQL column expression.
|
||||
* @param bool $allow_datetime Whether to accept a full legacy datetime.
|
||||
* @return string
|
||||
*/
|
||||
function events_admin_date_meta_expression( $column, $allow_datetime = false ) {
|
||||
$datetime = $allow_datetime ? "WHEN {$column} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$' THEN STR_TO_DATE({$column}, '%Y-%m-%d %H:%i:%s') " : '';
|
||||
return "CASE {$datetime}WHEN {$column} REGEXP '^[0-9]{8}$' THEN STR_TO_DATE({$column}, '%Y%m%d') WHEN {$column} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' THEN STR_TO_DATE({$column}, '%Y-%m-%d') ELSE NULL END";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the deterministic admin occurrence ORDER BY expression.
|
||||
*
|
||||
* @param string $posts_table Static posts table name.
|
||||
* @param string $order Requested direction.
|
||||
* @return string
|
||||
*/
|
||||
function events_admin_date_sort_orderby_sql( $posts_table, $order ) {
|
||||
$order = 'DESC' === strtoupper( (string) $order ) ? 'DESC' : 'ASC';
|
||||
$postmeta_table = events_date_sort_postmeta_table();
|
||||
$canonical_expression = events_admin_date_meta_expression( 'aa_sort.meta_value', true );
|
||||
$date_expression = events_admin_date_meta_expression( 'aa_er.meta_value' );
|
||||
$datetime_expression = events_admin_date_meta_expression( 'aa_edt.meta_value', true );
|
||||
$legacy_expression = events_admin_date_meta_expression( 'aa_d.meta_value' );
|
||||
$start_time_value = "(SELECT aa_time.meta_value FROM {$postmeta_table} AS aa_time WHERE aa_time.post_id = aa_er.post_id AND aa_time.meta_key = CONCAT(LEFT(aa_er.meta_key, LENGTH(aa_er.meta_key) - 4), 'start_time') ORDER BY aa_time.meta_id ASC LIMIT 1)";
|
||||
$all_day_value = "(SELECT aa_all_day.meta_value FROM {$postmeta_table} AS aa_all_day WHERE aa_all_day.post_id = aa_er.post_id AND aa_all_day.meta_key = CONCAT(LEFT(aa_er.meta_key, LENGTH(aa_er.meta_key) - 4), 'all_day') ORDER BY aa_all_day.meta_id ASC LIMIT 1)";
|
||||
$start_time_expression = "CASE WHEN LOWER(TRIM(COALESCE({$all_day_value}, ''))) IN ('1', 'true', 'yes', 'on') THEN STR_TO_DATE('00:00:00', '%H:%i:%s') WHEN {$start_time_value} REGEXP '^([01][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$' THEN CASE WHEN CHAR_LENGTH({$start_time_value}) = 5 THEN STR_TO_DATE({$start_time_value}, '%H:%i') ELSE STR_TO_DATE({$start_time_value}, '%H:%i:%s') END ELSE STR_TO_DATE('00:00:00', '%H:%i:%s') END";
|
||||
$repeater = "(SELECT MIN(TIMESTAMP({$date_expression}, {$start_time_expression})) FROM {$postmeta_table} AS aa_er WHERE aa_er.post_id = {$posts_table}.ID AND aa_er.meta_key REGEXP '^event_dates_[0-9]+_date$')";
|
||||
$legacy_datetime = "(SELECT MIN({$datetime_expression}) FROM {$postmeta_table} AS aa_edt WHERE aa_edt.post_id = {$posts_table}.ID AND aa_edt.meta_key = 'event_datetime')";
|
||||
$legacy_date = "(SELECT MIN({$legacy_expression}) FROM {$postmeta_table} AS aa_d WHERE aa_d.post_id = {$posts_table}.ID AND aa_d.meta_key = 'date')";
|
||||
return "COALESCE({$canonical_expression}, {$repeater}, {$legacy_datetime}, {$legacy_date}, {$posts_table}.post_date) {$order}, {$posts_table}.post_date {$order}, {$posts_table}.ID {$order}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the postmeta table used by date-sort SQL helpers.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function events_date_sort_postmeta_table() {
|
||||
global $wpdb;
|
||||
return isset( $wpdb->postmeta ) ? $wpdb->postmeta : 'wp_postmeta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build grouped joins for normalized event date sort values.
|
||||
*
|
||||
* @param string $postmeta_table Static postmeta table name.
|
||||
* @param string $posts_table Static posts table name.
|
||||
* @return string
|
||||
*/
|
||||
function events_admin_date_sort_join_sql( $postmeta_table, $posts_table ) {
|
||||
return " LEFT JOIN {$postmeta_table} AS aa_sort ON aa_sort.meta_id = (SELECT MIN(aa_sort_pick.meta_id) FROM {$postmeta_table} AS aa_sort_pick WHERE aa_sort_pick.post_id = {$posts_table}.ID AND aa_sort_pick.meta_key = '_aa_events_sort_start')";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an occurrence sort flag belongs to a supported event query.
|
||||
*
|
||||
* @param WP_Query $query Query object.
|
||||
* @return bool
|
||||
*/
|
||||
function events_date_sort_query_applies( $query ) {
|
||||
if ( ! $query->is_main_query() || ! $query->get( 'aa_events_date_sort' ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( is_admin() ) {
|
||||
return 'event' === $query->get( 'post_type' );
|
||||
}
|
||||
return $query->is_post_type_archive( 'event' ) || $query->is_tax( 'event_type' ) || $query->is_tax( 'event_tag' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add occurrence date sort joins to a flagged main query.
|
||||
*
|
||||
* @param string $join SQL JOIN clause.
|
||||
* @param WP_Query $query Query object.
|
||||
* @return string
|
||||
*/
|
||||
function events_date_sort_posts_join( $join, $query ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! events_date_sort_query_applies( $query ) ) {
|
||||
return $join;
|
||||
}
|
||||
|
||||
return $join . events_admin_date_sort_join_sql( $wpdb->postmeta, $wpdb->posts );
|
||||
}
|
||||
add_filter( 'posts_join', 'events_date_sort_posts_join', 10, 2 );
|
||||
|
||||
/**
|
||||
* Apply deterministic occurrence date ordering to a flagged main query.
|
||||
*
|
||||
* @param string $orderby SQL ORDER BY clause.
|
||||
* @param WP_Query $query Query object.
|
||||
* @return string
|
||||
*/
|
||||
function events_date_sort_posts_orderby( $orderby, $query ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! events_date_sort_query_applies( $query ) ) {
|
||||
return $orderby;
|
||||
}
|
||||
|
||||
// Priority: earliest repeater date -> legacy datetime -> legacy date -> post date.
|
||||
return events_admin_date_sort_orderby_sql( $wpdb->posts, $query->get( 'order' ) );
|
||||
}
|
||||
add_filter( 'posts_orderby', 'events_date_sort_posts_orderby', 10, 2 );
|
||||
|
||||
/**
|
||||
* Extend REST API to allow ordering events by meta value.
|
||||
* Adds support for `orderby=meta_value|meta_value_num` and `meta_key`.
|
||||
@@ -287,8 +415,7 @@ function events_rest_adjust_query_for_meta_ordering( $args, $request ) {
|
||||
add_filter( 'rest_event_query', 'events_rest_adjust_query_for_meta_ordering', 10, 2 );
|
||||
|
||||
/**
|
||||
* Set default sort order for event post type by event_datetime ascending.
|
||||
* Applies to both admin and frontend queries.
|
||||
* Set occurrence-aware default sort order for event list queries.
|
||||
*
|
||||
* @param WP_Query $query The WordPress query object.
|
||||
*/
|
||||
@@ -298,17 +425,19 @@ function events_set_default_sort_order( $query ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin list table
|
||||
// Admin list table: default to event start-date sorting.
|
||||
if ( is_admin() && $query->get( 'post_type' ) === 'event' ) {
|
||||
if ( ! $query->get( 'orderby' ) ) {
|
||||
$query->set( 'meta_key', 'event_datetime' );
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
$query->set( 'aa_events_date_sort', 1 );
|
||||
$query->set( 'orderby', 'none' );
|
||||
$query->set( 'order', 'ASC' );
|
||||
}
|
||||
}
|
||||
|
||||
// Frontend: event archives and taxonomies
|
||||
if ( ! is_admin() && $query->get( 'post_type' ) === 'event' ) {
|
||||
// Frontend: event archives and taxonomies. Taxonomy queries do not always expose post_type.
|
||||
$is_event_archive = ! is_admin() && $query->is_post_type_archive( 'event' );
|
||||
$is_event_taxonomy = ! is_admin() && ( $query->is_tax( 'event_type' ) || $query->is_tax( 'event_tag' ) );
|
||||
if ( $is_event_archive || $is_event_taxonomy ) {
|
||||
$map = events_get_sortable_meta_map();
|
||||
$orderby = $query->get( 'orderby' );
|
||||
|
||||
@@ -326,20 +455,18 @@ function events_set_default_sort_order( $query ) {
|
||||
}
|
||||
|
||||
// Main event archive: newest first (descending by date/time)
|
||||
if ( $query->is_post_type_archive( 'event' ) ) {
|
||||
if ( $is_event_archive ) {
|
||||
if ( ! $query->get( 'orderby' ) ) {
|
||||
$query->set( 'meta_key', 'event_datetime' );
|
||||
$query->set( 'meta_type', 'DATETIME' );
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
$query->set( 'aa_events_date_sort', 1 );
|
||||
$query->set( 'orderby', 'none' );
|
||||
$query->set( 'order', 'DESC' );
|
||||
}
|
||||
}
|
||||
|
||||
// Taxonomy views: upcoming first (ascending)
|
||||
if ( ( $query->is_tax( 'event_type' ) || $query->is_tax( 'event_tag' ) ) && ! $query->get( 'orderby' ) ) {
|
||||
$query->set( 'meta_key', 'event_datetime' );
|
||||
$query->set( 'meta_type', 'DATETIME' );
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
if ( $is_event_taxonomy && ! $query->get( 'orderby' ) ) {
|
||||
$query->set( 'aa_events_date_sort', 1 );
|
||||
$query->set( 'orderby', 'none' );
|
||||
$query->set( 'order', 'ASC' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,23 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Build safe contact-email markup for event templates.
|
||||
*
|
||||
* @param mixed $value Raw contact email value.
|
||||
* @return string Escaped mailto anchor, or an empty string for invalid input.
|
||||
*/
|
||||
function aa_events_contact_email_markup( $value ) {
|
||||
$raw = trim( (string) $value );
|
||||
$email = sanitize_email( $raw );
|
||||
if ( '' === $email || $email !== $raw ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$protected = antispambot( $email );
|
||||
return '<a href="' . esc_attr( 'mailto:' . $protected ) . '">' . wp_kses_post( $protected ) . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a template.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user