Add event contact email and multi-day occurrences design specifications
- Introduced a new specification for adding an optional contact email field to events, including front-end output and compatibility testing. - Added a comprehensive design document for multi-day event occurrences, detailing the new occurrence data model, validation, normalization, and calendar architecture. - Created specifications for ACF field-key switching compatibility and local ACF override compatibility to ensure seamless integration with existing themes. - Implemented a desktop calendar view toggle design, allowing users to switch between calendar and list views, with persistent preferences and responsive behavior.
This commit is contained in:
@@ -71,19 +71,12 @@ function events_has_admin_field_group() {
|
||||
*
|
||||
* Skips registration when an admin-created field group is already
|
||||
* assigned to the event post type, allowing full editorial control.
|
||||
*
|
||||
* 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 ( events_has_admin_field_group() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( function_exists( 'acf_add_local_field_group' ) ) :
|
||||
|
||||
acf_add_local_field_group(
|
||||
|
||||
@@ -70,7 +70,7 @@ final class AA_Events {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles.
|
||||
* Enqueue frontend assets.
|
||||
*/
|
||||
public function enqueue_styles() {
|
||||
$theme_stylesheet = get_stylesheet_directory() . '/aa-events/aa-events.css';
|
||||
@@ -90,6 +90,14 @@ final class AA_Events {
|
||||
AA_EVENTS_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'aa-events-calendar-view',
|
||||
AA_EVENTS_PLUGIN_URL . 'assets/js/aa-events-calendar-view.js',
|
||||
array(),
|
||||
AA_EVENTS_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+134
-10
@@ -59,6 +59,22 @@ function aa_events_occurrence_boolean( $value ) {
|
||||
return null !== $value && in_array( strtolower( $value ), array( '1', 'true', 'yes', 'on' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date returned by the built-in or supported override ACF fields.
|
||||
*
|
||||
* @param mixed $value Raw date.
|
||||
* @return string|null
|
||||
*/
|
||||
function aa_events_parse_occurrence_date( $value ) {
|
||||
foreach ( array( 'Y-m-d', 'Ymd', 'F j, Y' ) as $format ) {
|
||||
$date = aa_events_parse_occurrence_value( $value, $format, 'Y-m-d' );
|
||||
if ( null !== $date ) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a time with or without seconds.
|
||||
*
|
||||
@@ -66,8 +82,44 @@ function aa_events_occurrence_boolean( $value ) {
|
||||
* @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;
|
||||
foreach ( array( 'H:i:s', 'H:i', 'g:i a' ) as $format ) {
|
||||
$time = aa_events_parse_occurrence_value( $value, $format, 'H:i:s' );
|
||||
if ( null !== $time ) {
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a field through its active ACF definition when possible.
|
||||
*
|
||||
* Hidden ACF metadata references can point to the inactive built-in or
|
||||
* override field key when both groups use the same field name. Resolving the
|
||||
* active definition by name keeps reads independent of that stale reference.
|
||||
*
|
||||
* @param string $name Field name.
|
||||
* @param int $post_id Event post ID.
|
||||
* @return mixed
|
||||
*/
|
||||
function aa_events_get_active_field_value( $name, $post_id ) {
|
||||
if ( function_exists( 'acf_get_field' ) && function_exists( 'acf_get_value' ) && function_exists( 'acf_format_value' ) ) {
|
||||
$field = acf_get_field( $name );
|
||||
if ( is_array( $field ) && $name === ( $field['name'] ?? '' ) ) {
|
||||
$value = acf_format_value( acf_get_value( $post_id, $field ), $post_id, $field );
|
||||
if ( is_array( $value ) ) {
|
||||
return $value;
|
||||
}
|
||||
if ( function_exists( 'get_post_meta' ) ) {
|
||||
$raw_value = get_post_meta( $post_id, $name, true );
|
||||
if ( is_array( $raw_value ) ) {
|
||||
return $raw_value;
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return get_field( $name, $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +181,7 @@ function aa_events_occurrence_range_is_valid( $start_date, $start_time, $end_dat
|
||||
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' );
|
||||
$start_date = aa_events_parse_occurrence_date( $row['start_date'] ?? $row['date'] ?? '' );
|
||||
if ( null === $start_date ) {
|
||||
return null;
|
||||
}
|
||||
@@ -138,7 +190,7 @@ function aa_events_normalize_occurrence_row( array $row, $source = 'event_dates'
|
||||
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' );
|
||||
$end_date = '' === $end_input ? $start_date : aa_events_parse_occurrence_date( $end_input );
|
||||
if ( null === $end_date ) {
|
||||
return null;
|
||||
}
|
||||
@@ -187,9 +239,13 @@ function aa_events_normalize_occurrence_row( array $row, $source = 'event_dates'
|
||||
*/
|
||||
function aa_events_get_occurrences( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
$rows = get_field( 'event_dates', $post_id );
|
||||
$rows = aa_events_get_active_field_value( 'event_dates', $post_id );
|
||||
$occurrences = array();
|
||||
|
||||
if ( ! is_array( $rows ) || ! $rows ) {
|
||||
$rows = aa_events_get_active_field_value( 'multiple_dates', $post_id );
|
||||
}
|
||||
|
||||
if ( is_array( $rows ) && $rows ) {
|
||||
foreach ( $rows as $row ) {
|
||||
if ( is_array( $row ) ) {
|
||||
@@ -297,7 +353,7 @@ function events_get_all_event_dates( $post_id ) {
|
||||
* @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' );
|
||||
$start = aa_events_parse_occurrence_date( $row['start_date'] ?? $row['date'] ?? '' );
|
||||
if ( null === $start ) {
|
||||
return __( 'Start date is required and must be valid.', 'aa-events' );
|
||||
}
|
||||
@@ -305,7 +361,7 @@ function aa_events_validate_occurrence_row( array $row ) {
|
||||
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' );
|
||||
$end = '' === $end_input ? $start : aa_events_parse_occurrence_date( $end_input );
|
||||
if ( null === $end ) {
|
||||
return __( 'End date must be valid.', 'aa-events' );
|
||||
}
|
||||
@@ -446,6 +502,73 @@ function aa_events_occurrence_time_markup( array $occurrence ) {
|
||||
. ' <time datetime="' . esc_attr( $end_value ) . '">' . esc_html( $end_label ) . '</time>.</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy legacy local-override repeater rows into the canonical repeater.
|
||||
*
|
||||
* Existing legacy metadata is intentionally preserved. Canonical rows always
|
||||
* win, making this migration safe to run repeatedly.
|
||||
*
|
||||
* @param int $post_id Event post ID.
|
||||
* @return bool Whether canonical rows were written.
|
||||
*/
|
||||
function aa_events_migrate_multiple_dates_occurrences( $post_id ) {
|
||||
$post_id = (int) $post_id;
|
||||
if ( ! $post_id || 'event' !== get_post_type( $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$write_rows = static function ( $source_rows ) use ( $post_id ) {
|
||||
$rows = array();
|
||||
foreach ( $source_rows as $source_row ) {
|
||||
if ( ! is_array( $source_row ) ) {
|
||||
continue;
|
||||
}
|
||||
$occurrence = aa_events_normalize_occurrence_row( $source_row, 'event_dates', $post_id );
|
||||
if ( null === $occurrence ) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = array(
|
||||
'date' => $occurrence['start_date'],
|
||||
'start_time' => $occurrence['start_time'],
|
||||
'end_date' => empty( $source_row['end_date'] ) ? '' : $occurrence['end_date'],
|
||||
'end_time' => $occurrence['end_time'],
|
||||
'all_day' => $occurrence['all_day'] ? 1 : 0,
|
||||
);
|
||||
}
|
||||
if ( ! $rows ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$selector = 'event_dates';
|
||||
if ( function_exists( 'acf_get_field' ) ) {
|
||||
$field = acf_get_field( 'event_dates' );
|
||||
if ( is_array( $field ) && ! empty( $field['key'] ) ) {
|
||||
$selector = $field['key'];
|
||||
}
|
||||
}
|
||||
return (bool) update_field( $selector, $rows, $post_id );
|
||||
};
|
||||
|
||||
if ( function_exists( 'get_post_meta' ) ) {
|
||||
$raw_canonical_rows = get_post_meta( $post_id, 'event_dates', true );
|
||||
if ( is_array( $raw_canonical_rows ) && $raw_canonical_rows ) {
|
||||
return $write_rows( $raw_canonical_rows );
|
||||
}
|
||||
}
|
||||
|
||||
$canonical_rows = aa_events_get_active_field_value( 'event_dates', $post_id );
|
||||
if ( is_array( $canonical_rows ) && $canonical_rows ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$legacy_rows = aa_events_get_active_field_value( 'multiple_dates', $post_id );
|
||||
if ( ! is_array( $legacy_rows ) || ! $legacy_rows ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $write_rows( $legacy_rows );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the first parseable legacy occurrence into the repeater.
|
||||
*
|
||||
@@ -487,6 +610,7 @@ function aa_events_migrate_legacy_on_save( $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_multiple_dates_occurrences( $post_id );
|
||||
aa_events_migrate_legacy_occurrence( $post_id );
|
||||
}
|
||||
|
||||
@@ -579,11 +703,11 @@ function aa_events_run_occurrence_index_backfill() {
|
||||
* @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 ) ) {
|
||||
if ( in_array( $meta_key, array( 'event_dates', 'multiple_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 );
|
||||
return (bool) preg_match( '/^(event_dates|multiple_dates)_[0-9]+_(date|start_time|end_date|end_time|all_day)$/', $meta_key );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,7 +747,7 @@ function aa_events_run_queued_sort_sync() {
|
||||
|
||||
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 ) {
|
||||
foreach ( array( 'event_dates', 'multiple_dates', 'event_datetime', 'date', 'start_time' ) as $field_name ) {
|
||||
acf_flush_value_cache( $post_id, $field_name );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +262,14 @@ function events_admin_date_sort_orderby_sql( $posts_table, $order ) {
|
||||
$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_repeater_date = events_admin_date_meta_expression( 'aa_mr.meta_value' );
|
||||
$legacy_time_value = "(SELECT aa_mtime.meta_value FROM {$postmeta_table} AS aa_mtime WHERE aa_mtime.post_id = aa_mr.post_id AND aa_mtime.meta_key = CONCAT(LEFT(aa_mr.meta_key, LENGTH(aa_mr.meta_key) - 4), 'start_time') ORDER BY aa_mtime.meta_id ASC LIMIT 1)";
|
||||
$legacy_all_day_value = "(SELECT aa_mall.meta_value FROM {$postmeta_table} AS aa_mall WHERE aa_mall.post_id = aa_mr.post_id AND aa_mall.meta_key = CONCAT(LEFT(aa_mr.meta_key, LENGTH(aa_mr.meta_key) - 4), 'all_day') ORDER BY aa_mall.meta_id ASC LIMIT 1)";
|
||||
$legacy_time_expression = "CASE WHEN LOWER(TRIM(COALESCE({$legacy_all_day_value}, ''))) IN ('1', 'true', 'yes', 'on') THEN STR_TO_DATE('00:00:00', '%H:%i:%s') WHEN {$legacy_time_value} REGEXP '^([01][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$' THEN CASE WHEN CHAR_LENGTH({$legacy_time_value}) = 5 THEN STR_TO_DATE({$legacy_time_value}, '%H:%i') ELSE STR_TO_DATE({$legacy_time_value}, '%H:%i:%s') END ELSE STR_TO_DATE('00:00:00', '%H:%i:%s') END";
|
||||
$legacy_repeater = "(SELECT MIN(TIMESTAMP({$legacy_repeater_date}, {$legacy_time_expression})) FROM {$postmeta_table} AS aa_mr WHERE aa_mr.post_id = {$posts_table}.ID AND aa_mr.meta_key REGEXP '^multiple_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 "COALESCE({$canonical_expression}, {$repeater}, {$legacy_repeater}, {$legacy_datetime}, {$legacy_date}, {$posts_table}.post_date) {$order}, {$posts_table}.post_date {$order}, {$posts_table}.ID {$order}";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user