This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,422 @@
<?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
}