423 lines
15 KiB
PHP
423 lines
15 KiB
PHP
<?php
|
|
/*
|
|
|
|
This file is part of a child theme called Hello GeneratePress.
|
|
Functions in this file will be loaded before the parent theme's functions.
|
|
For more information, please read
|
|
https://developer.wordpress.org/themes/advanced-topics/child-themes/
|
|
|
|
*/
|
|
|
|
// Loads the parent stylesheet — leave this in place unless you know what you're doing.
|
|
function your_theme_enqueue_styles() {
|
|
|
|
$parent_style = 'parent-style';
|
|
|
|
wp_enqueue_style( $parent_style,
|
|
get_template_directory_uri() . '/style.css'
|
|
);
|
|
|
|
wp_enqueue_style( 'child-style',
|
|
get_stylesheet_directory_uri() . '/style.css',
|
|
array( $parent_style ),
|
|
wp_get_theme()->get( 'Version' )
|
|
);
|
|
}
|
|
add_action( 'wp_enqueue_scripts', 'your_theme_enqueue_styles' );
|
|
|
|
|
|
// force meta boxes panel in gutenberg editor to be full natual height
|
|
add_action('admin_head', function() {
|
|
echo '<style>
|
|
.edit-post-meta-boxes-main,
|
|
.edit-post-layout__metaboxes{
|
|
overflow: visible !important;
|
|
}
|
|
</style>';
|
|
});
|
|
|
|
// 4. Remove the Comments link from the Admin Sidebar menu
|
|
add_action('admin_menu', 'disable_comment_admin_menu');
|
|
function disable_comment_admin_menu() {
|
|
remove_menu_page('edit-comments.php');
|
|
}
|
|
|
|
|
|
// Change gravity form error message from h2 to span
|
|
add_filter( 'gform_validation_message', 'change_message', 10, 2 );
|
|
function change_message( $message, $form ) {
|
|
return "<span>There was a problem with your submission. Please review the fields below.</span>";
|
|
}
|
|
// Change gravity form required legend text
|
|
add_filter( 'gform_required_legend', function( $legend, $form ) {
|
|
return 'An asterisk (*) indicates a required field.';
|
|
}, 10, 2 );
|
|
// force AJAX on all GForms
|
|
add_filter( 'gform_form_args', 'setup_form_args' );
|
|
function setup_form_args( $form_args ) {
|
|
$form_args['ajax'] = true;
|
|
return $form_args;
|
|
}
|
|
/**
|
|
* Gravity Forms Date Field Validation
|
|
*
|
|
* ACCESSIBILITY FEATURES:
|
|
* - Server-side validation with descriptive error messages.
|
|
* - Client-side real-time feedback with aria-live announcements.
|
|
* - Error messages clearly identify which field has the problem.
|
|
* - End date min attribute updates dynamically when start date changes.
|
|
*/
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// CONFIGURATION
|
|
// ─────────────────────────────────────────────────────────────
|
|
define( 'GF_DATE_FORM_ID', 5 ); // Form ID
|
|
define( 'GF_START_DATE_FIELD_ID', 8 ); // Start Date field ID
|
|
define( 'GF_END_DATE_FIELD_ID', 9 ); // End Date field ID
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
/**
|
|
* Parse a yyyy-mm-dd or yyyy/mm/dd string into a DateTime object.
|
|
* Returns false if the string is empty or does not match either format.
|
|
*
|
|
* @param string $raw
|
|
* @return DateTime|false
|
|
*/
|
|
function gf_parse_ymd( $raw ) {
|
|
if ( empty( $raw ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Normalise: replace slash separator with dash so one format string handles both.
|
|
$normalised = str_replace( '/', '-', trim( $raw ) );
|
|
|
|
// Reject anything that is not exactly yyyy-mm-dd after normalisation.
|
|
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $normalised ) ) {
|
|
return false;
|
|
}
|
|
|
|
$date = DateTime::createFromFormat( 'Y-m-d', $normalised );
|
|
|
|
// createFromFormat can return a date even when the values overflow
|
|
// (e.g. month 13). Verify the parsed values match the input.
|
|
if ( ! $date ) {
|
|
return false;
|
|
}
|
|
|
|
$date->setTime( 0, 0, 0 );
|
|
|
|
// Guard against overflow dates (e.g. 2026-13-01 parsing as 2027-01-01).
|
|
if ( $date->format( 'Y-m-d' ) !== $normalised ) {
|
|
return false;
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
|
|
/**
|
|
* SERVER-SIDE VALIDATION
|
|
* Runs on form submission. Catches bad dates even if JS is bypassed.
|
|
*/
|
|
add_filter( 'gform_validation', 'gf_validate_date_range' );
|
|
|
|
function gf_validate_date_range( $validation_result ) {
|
|
$form = $validation_result['form'];
|
|
|
|
if ( (int) $form['id'] !== GF_DATE_FORM_ID ) {
|
|
return $validation_result;
|
|
}
|
|
|
|
$start_raw = isset( $_POST[ 'input_' . GF_START_DATE_FIELD_ID ] )
|
|
? sanitize_text_field( $_POST[ 'input_' . GF_START_DATE_FIELD_ID ] )
|
|
: '';
|
|
|
|
$end_raw = isset( $_POST[ 'input_' . GF_END_DATE_FIELD_ID ] )
|
|
? sanitize_text_field( $_POST[ 'input_' . GF_END_DATE_FIELD_ID ] )
|
|
: '';
|
|
|
|
$start_date = gf_parse_ymd( $start_raw );
|
|
$end_date = gf_parse_ymd( $end_raw );
|
|
$today = new DateTime( 'today midnight' );
|
|
|
|
foreach ( $form['fields'] as &$field ) {
|
|
|
|
// ── Start Date ─────────────────────────────────────────────────────
|
|
if ( (int) $field->id === GF_START_DATE_FIELD_ID ) {
|
|
if ( ! $start_date ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = 'Please enter a valid start date.';
|
|
$validation_result['is_valid'] = false;
|
|
} elseif ( $start_date < $today ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = 'Start date must be today or a future date.';
|
|
$validation_result['is_valid'] = false;
|
|
}
|
|
}
|
|
|
|
// ── End Date ───────────────────────────────────────────────────────
|
|
if ( (int) $field->id === GF_END_DATE_FIELD_ID ) {
|
|
if ( ! $end_date ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = 'Please enter a valid end date.';
|
|
$validation_result['is_valid'] = false;
|
|
} elseif ( $start_date && $end_date < $start_date ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = 'End date must be on or after the start date (' . $start_date->format( 'Y-m-d' ) . ').';
|
|
$validation_result['is_valid'] = false;
|
|
} elseif ( ! $start_date && $end_date < $today ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = 'End date must be today or a future date.';
|
|
$validation_result['is_valid'] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
$validation_result['form'] = $form;
|
|
return $validation_result;
|
|
}
|
|
|
|
|
|
/**
|
|
* CLIENT-SIDE VALIDATION
|
|
* - Locks start date min to today.
|
|
* - Updates end date min whenever start date changes.
|
|
* - Shows accessible inline errors on change and blur.
|
|
* - Handles both yyyy-mm-dd and yyyy/mm/dd field values.
|
|
*/
|
|
add_action( 'wp_footer', 'gf_date_range_scripts' );
|
|
|
|
function gf_date_range_scripts() {
|
|
if ( ! class_exists( 'GFForms' ) ) {
|
|
return;
|
|
}
|
|
?>
|
|
<script>
|
|
( function () {
|
|
|
|
var FORM_ID = <?php echo (int) GF_DATE_FORM_ID; ?>;
|
|
var START_FIELD_ID = <?php echo (int) GF_START_DATE_FIELD_ID; ?>;
|
|
var END_FIELD_ID = <?php echo (int) GF_END_DATE_FIELD_ID; ?>;
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
/** Format a Date as yyyy-mm-dd (for the min attribute and error messages). */
|
|
function toYMD( date ) {
|
|
var y = date.getFullYear();
|
|
var m = String( date.getMonth() + 1 ).padStart( 2, '0' );
|
|
var d = String( date.getDate() ).padStart( 2, '0' );
|
|
return y + '-' + m + '-' + d;
|
|
}
|
|
|
|
/**
|
|
* Parse a yyyy-mm-dd or yyyy/mm/dd string into a midnight Date.
|
|
* Returns null if the value is empty or does not match either format.
|
|
*
|
|
* Uses explicit part splitting rather than new Date(string) to avoid
|
|
* browser timezone inconsistencies (ISO strings are parsed as UTC,
|
|
* which shifts the date in negative-offset timezones).
|
|
*/
|
|
function parseYMD( val ) {
|
|
if ( ! val ) return null;
|
|
val = val.trim();
|
|
|
|
// Accept both separators.
|
|
if ( ! /^\d{4}[\/\-]\d{2}[\/\-]\d{2}$/.test( val ) ) return null;
|
|
|
|
var parts = val.split( /[\/\-]/ );
|
|
var yr = parseInt( parts[0], 10 );
|
|
var mo = parseInt( parts[1], 10 );
|
|
var dy = parseInt( parts[2], 10 );
|
|
|
|
// Basic range check before constructing the Date.
|
|
if ( mo < 1 || mo > 12 || dy < 1 || dy > 31 ) return null;
|
|
|
|
// new Date(y, m-1, d) is always local time — no UTC shift.
|
|
var d = new Date( yr, mo - 1, dy );
|
|
d.setHours( 0, 0, 0, 0 );
|
|
|
|
// Guard against overflow (e.g. Feb 30 rolling into March).
|
|
if ( d.getFullYear() !== yr || d.getMonth() !== mo - 1 || d.getDate() !== dy ) {
|
|
return null;
|
|
}
|
|
|
|
return d;
|
|
}
|
|
|
|
/** Get the input element for a GF date field. */
|
|
function getInput( fieldId ) {
|
|
return document.getElementById( 'input_' + FORM_ID + '_' + fieldId );
|
|
}
|
|
|
|
// ── Error display ─────────────────────────────────────────────────────
|
|
|
|
function setFieldError( fieldId, message ) {
|
|
var errorId = 'gf-date-error-' + fieldId;
|
|
var inputEl = getInput( fieldId );
|
|
if ( ! inputEl ) return;
|
|
|
|
var container = inputEl.closest( '.gfield' );
|
|
if ( ! container ) return;
|
|
|
|
var existing = document.getElementById( errorId );
|
|
|
|
if ( message ) {
|
|
inputEl.setAttribute( 'aria-invalid', 'true' );
|
|
inputEl.setAttribute( 'aria-describedby', errorId );
|
|
|
|
if ( ! existing ) {
|
|
var errorEl = document.createElement( 'span' );
|
|
errorEl.id = errorId;
|
|
errorEl.className = 'gf-date-inline-error';
|
|
// role="alert" + aria-live="assertive" announces immediately
|
|
// even if focus has already moved away from the field.
|
|
errorEl.setAttribute( 'role', 'alert' );
|
|
errorEl.setAttribute( 'aria-live', 'assertive' );
|
|
errorEl.setAttribute( 'aria-atomic', 'true' );
|
|
errorEl.style.cssText = [
|
|
'display:block',
|
|
'margin-top:4px',
|
|
'color:#d32f2f',
|
|
'font-size:0.875em',
|
|
'font-weight:600',
|
|
].join( ';' );
|
|
inputEl.parentNode.insertBefore( errorEl, inputEl.nextSibling );
|
|
existing = errorEl;
|
|
}
|
|
|
|
existing.textContent = message;
|
|
|
|
} else {
|
|
inputEl.removeAttribute( 'aria-invalid' );
|
|
inputEl.removeAttribute( 'aria-describedby' );
|
|
if ( existing ) existing.remove();
|
|
}
|
|
}
|
|
|
|
// ── Datepicker UTC fix ────────────────────────────────────────────────
|
|
//
|
|
// GF uses jQuery UI datepicker. When a user clicks a date, jQuery UI
|
|
// constructs a UTC midnight Date object for that day, then formats it
|
|
// into the input. In negative UTC offsets (e.g. Eastern = UTC-4), UTC
|
|
// midnight is the previous calendar day in local time, so the datepicker
|
|
// writes the day AFTER the one you clicked.
|
|
//
|
|
// Fix: hook into the datepicker's beforeShowDay / onSelect via jQuery UI's
|
|
// option API after GF initialises the picker, and rewrite the value using
|
|
// the raw dateText string (which is always the correct clicked date)
|
|
// rather than letting jQuery UI derive it from a UTC Date object.
|
|
|
|
function fixDatepickerUTC( inputEl ) {
|
|
if ( typeof jQuery === 'undefined' || ! jQuery( inputEl ).datepicker ) return;
|
|
|
|
try {
|
|
// Wrap the existing onSelect if there is one.
|
|
var existing = jQuery( inputEl ).datepicker( 'option', 'onSelect' ) || function() {};
|
|
|
|
jQuery( inputEl ).datepicker( 'option', 'onSelect', function( dateText, inst ) {
|
|
// dateText is the raw string the user clicked, e.g. "2026-06-29".
|
|
// Write it directly — bypasses the UTC Date conversion entirely.
|
|
inputEl.value = dateText;
|
|
|
|
// Fire the original onSelect GF attached (updates field state, etc.).
|
|
existing.call( this, dateText, inst );
|
|
|
|
// Now validate with the corrected value.
|
|
validateDates();
|
|
} );
|
|
} catch (e) {
|
|
// If datepicker isn't initialised yet, GF fires gform_post_render
|
|
// which re-inits pickers — we re-apply the fix there (see below).
|
|
}
|
|
}
|
|
|
|
// ── Core validation ───────────────────────────────────────────────────
|
|
|
|
function validateDates() {
|
|
var today = new Date();
|
|
today.setHours( 0, 0, 0, 0 );
|
|
|
|
var startInput = getInput( START_FIELD_ID );
|
|
var endInput = getInput( END_FIELD_ID );
|
|
|
|
var startDate = startInput ? parseYMD( startInput.value ) : null;
|
|
var endDate = endInput ? parseYMD( endInput.value ) : null;
|
|
|
|
// ── Start date ───────────────────────────────────────────────────
|
|
if ( startDate && startDate < today ) {
|
|
setFieldError( START_FIELD_ID, 'Start date must be today or a future date.' );
|
|
} else {
|
|
setFieldError( START_FIELD_ID, '' );
|
|
}
|
|
|
|
// ── Update end date minimum ───────────────────────────────────────
|
|
var minForEnd = ( startDate && startDate >= today ) ? startDate : today;
|
|
if ( endInput ) {
|
|
endInput.setAttribute( 'min', toYMD( minForEnd ) );
|
|
}
|
|
|
|
// ── End date ─────────────────────────────────────────────────────
|
|
if ( endDate ) {
|
|
if ( startDate && startDate >= today && endDate < startDate ) {
|
|
setFieldError(
|
|
END_FIELD_ID,
|
|
'End date must be on or after the start date (' + toYMD( startDate ) + ').'
|
|
);
|
|
} else if ( endDate < today ) {
|
|
setFieldError( END_FIELD_ID, 'End date must be today or a future date.' );
|
|
} else {
|
|
setFieldError( END_FIELD_ID, '' );
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Attach listeners ──────────────────────────────────────────────────
|
|
|
|
function attachListeners() {
|
|
var startInput = getInput( START_FIELD_ID );
|
|
var endInput = getInput( END_FIELD_ID );
|
|
if ( ! startInput || ! endInput ) return false;
|
|
|
|
var today = new Date();
|
|
startInput.setAttribute( 'min', toYMD( today ) );
|
|
endInput.setAttribute( 'min', toYMD( today ) );
|
|
|
|
// Apply the datepicker UTC fix to both fields.
|
|
// We defer slightly to ensure GF has finished initialising the picker.
|
|
setTimeout( function() {
|
|
fixDatepickerUTC( startInput );
|
|
fixDatepickerUTC( endInput );
|
|
}, 100 );
|
|
|
|
// Re-apply after GF re-renders (e.g. multi-page forms, AJAX).
|
|
if ( typeof jQuery !== 'undefined' ) {
|
|
jQuery( document ).on( 'gform_post_render', function( e, formId ) {
|
|
if ( formId === FORM_ID ) {
|
|
setTimeout( function() {
|
|
fixDatepickerUTC( getInput( START_FIELD_ID ) );
|
|
fixDatepickerUTC( getInput( END_FIELD_ID ) );
|
|
}, 100 );
|
|
}
|
|
} );
|
|
}
|
|
|
|
// Blur listener as a safety net for typed-in values (not datepicker).
|
|
startInput.addEventListener( 'blur', validateDates );
|
|
endInput.addEventListener( 'blur', validateDates );
|
|
|
|
return true;
|
|
}
|
|
|
|
if ( ! attachListeners() ) {
|
|
var observer = new MutationObserver( function () {
|
|
if ( attachListeners() ) observer.disconnect();
|
|
} );
|
|
observer.observe( document.body, { childList: true, subtree: true } );
|
|
}
|
|
|
|
} )();
|
|
</script>
|
|
<?php
|
|
}
|