Skip to content

Instantly share code, notes, and snippets.

@only-cliches
Last active February 16, 2023 10:50
Show Gist options
  • Save only-cliches/f41e0aad1bcd043292a2705216a785d5 to your computer and use it in GitHub Desktop.
Save only-cliches/f41e0aad1bcd043292a2705216a785d5 to your computer and use it in GitHub Desktop.
Advanced Custom Fields Date/Time Picker V2
<?php
/*
!!!!! README !!!!
v1.3, released 2/15/23
Tested with ACF 5.12.3 and ACF Pro 5.12.3
Screenshot: https://i.imgur.com/IJlvzVp.png
How is this different from the default in date/time picker?
- The timezone will automatically adjust in the Wordpress Control Panel based on the admin's device/browser timezone.
- Does not look at, care about, or even consider the time zone setting in Wordpress.
- Date/time rendering/formatting to the front end is more manual. Documentation includes examples of rendering based on server timezone and rendering based on the user's device/browser timezone.
How is this the same as the default date/time picker?
- This field will save the selected Date/Time as a unix timestamp in seconds.
- Will happily read and take over a field that was previously set to the default date/time picker without changing the date/times already in place. Rendering/output behavior on the `get_field` call is different, though.
1. Installation
Put this file (class-acf-field-date_time_picker_v2.php) in the following directory:
(FREE VERSION)
/wp-content/plugins/advanced-custom-fields/includes/fields
(PRO VERSION)
/wp-content/plugins/advanced-custom-fields-pro/includes/fields
Copy down the `acf.php` file in the root of the plugin directory and make the following changes:
A. Find the line with this text: `acf_include( 'includes/fields/class-acf-field-date_time_picker.php' );` (should be around line 268)
B. Add this line BELOW the line you found in step A: `acf_include( 'includes/fields/class-acf-field-date_time_picker_v2.php' );`
C. Upload new `acf.php` file and profit.
(You will likely have to perform these installation steps every time you update ACF)
2. Usage
The field will be available as "Date Time Picker V2"
Two choices to render date/time:
A. Display according to SERVER timezone (wll be the same for all users)
<div id='my-date'>
<?php echo date('d F, Y (l)', get_field('my_date_field')); ?>
</div>
B. Display according to USER device/brwoser timezone
<div id='my-date'>
</div>
<script>
// grab date/time value from PHP and convert to ms
var date_value = <?php echo get_field('my_date_field'); ?> * 1000;
// render to document
document.getElementById("my-date").innerText = new Date(date_value).toLocaleString();
</script>
Lots of JS libraries make it easy to render custom formated date/time: https://moment.github.io/luxon/#/
Drop a comment if you have any questions! This file (or and updated version) should be available at:
https://gist.github.com/only-cliches/f41e0aad1bcd043292a2705216a785d5
*/
if ( ! class_exists( 'acf_field_date_and_time_picker_v2' ) ) :
class acf_field_date_and_time_picker_v2 extends acf_field {
/*
* __construct
*
* This function will setup the field type data
*
* @type function
* @date 5/03/2014
* @since 5.0.0
*
* @param n/a
* @return n/a
*/
function initialize() {
// vars
$this->name = 'date_time_picker_v2';
$this->label = __( 'Date Time Picker V2', 'acf' );
$this->category = 'jquery';
// $this->defaults = array(
// 'display_format' => 'd/m/Y g:i a',
// 'return_format' => 'd/m/Y g:i a',
// );
}
/*
* input_admin_enqueue_scripts
*
* description
*
* @type function
* @date 16/12/2015
* @since 5.3.2
*
* @param $post_id (int)
* @return $post_id (int)
*/
function input_admin_enqueue_scripts() {
// bail ealry if no enqueue
// if ( ! acf_get_setting( 'enqueue_datetimepicker' ) ) {
// return;
// }
// vars
$version = '1.6.1';
// // script
// wp_enqueue_script( 'acf-timepicker', acf_get_url( 'assets/inc/timepicker/jquery-ui-timepicker-addon.min.js' ), array( 'jquery-ui-datepicker' ), $version );
// // style
// wp_enqueue_style( 'acf-timepicker', acf_get_url( 'assets/inc/timepicker/jquery-ui-timepicker-addon.min.css' ), '', $version );
// localize
// acf_localize_data(
// array(
// 'dateTimePickerL10n' => array(
// 'timeOnlyTitle' => _x( 'Choose Time', 'Date Time Picker JS timeOnlyTitle', 'acf' ),
// 'timeText' => _x( 'Time', 'Date Time Picker JS timeText', 'acf' ),
// 'hourText' => _x( 'Hour', 'Date Time Picker JS hourText', 'acf' ),
// 'minuteText' => _x( 'Minute', 'Date Time Picker JS minuteText', 'acf' ),
// 'secondText' => _x( 'Second', 'Date Time Picker JS secondText', 'acf' ),
// 'millisecText' => _x( 'Millisecond', 'Date Time Picker JS millisecText', 'acf' ),
// 'microsecText' => _x( 'Microsecond', 'Date Time Picker JS microsecText', 'acf' ),
// 'timezoneText' => _x( 'Time Zone', 'Date Time Picker JS timezoneText', 'acf' ),
// 'currentText' => _x( 'Now', 'Date Time Picker JS currentText', 'acf' ),
// 'closeText' => _x( 'Done', 'Date Time Picker JS closeText', 'acf' ),
// 'selectText' => _x( 'Select', 'Date Time Picker JS selectText', 'acf' ),
// 'amNames' => array(
// _x( 'AM', 'Date Time Picker JS amText', 'acf' ),
// _x( 'A', 'Date Time Picker JS amTextShort', 'acf' ),
// ),
// 'pmNames' => array(
// _x( 'PM', 'Date Time Picker JS pmText', 'acf' ),
// _x( 'P', 'Date Time Picker JS pmTextShort', 'acf' ),
// ),
// ),
// )
// );
}
/*
* render_field()
*
* Create the HTML interface for your field
*
* @param $field - an array holding all the field's data
*
* @type action
* @since 3.6
* @date 23/01/13
*/
function render_field( $field ) {
// Set value.
$hidden_value = $field['value'];
$hidden_input = array(
'id' => $field['id'],
'class' => 'input-alt',
'name' => $field['name'],
'value' => $hidden_value,
);
// Output.
?>
<div id="container-<?php echo $field['id'] ?>">
<?php acf_hidden_input( $hidden_input ); ?>
<div class="date_group_v2">
<div>
<label style="opacity:0">--</label>
<div style="width:50px;" class="input-weekday">
</div>
</div>
<div>
<label>Month</label>
<select class="input-month">
<option value="0">(01) Jan</option>
<option value="1">(02) Feb</option>
<option value="2">(03) March</option>
<option value="3">(04) April</option>
<option value="4">(05) May</option>
<option value="5">(06) June</option>
<option value="6">(07) July</option>
<option value="7">(08) Aug</option>
<option value="8">(09) Sept</option>
<option value="9">(10) Oct</option>
<option value="10">(11) Nov</option>
<option value="11">(12) Dec</option>
</select>
</div>
<div>
<label>Date</label>
<input class="input-date" type="number" min="0" max="32" />
</div>
<div>
<label>Year</label>
<input class="input-year" type="number" min="1990"/>
</div>
<div style="width: 50px;">
<label>Hour</label>
<input class="input-hour" type="number" min="0" max="13"/>
</div>
<div style="width: 50px;">
<label>Minute</label>
<input class="input-minute" type="number" min="-1" max="60" />
</div>
<div style="width: 50px;">
<label style="opacity:0">--</label>
<select class="input-ampm">
<option></option>
<option value="am">AM</option>
<option value="pm">PM</option>
</select>
</div>
</div>
<div class="date_group_shortcutes">
<a class="prev_week" href="#">- Week</a>
<a class="prev_day" href="#">- Day</a>
<a class="prev_hour" href="#">- Hour</a>
<a class="today" href="#">Now</a>
<a class="next_hour" href="#">+ Hour</a>
<a class="next_day" href="#">+ Day</a>
<a class="next_week" href="#">+ Week</a>
</div>
<div class="date_group_timezone">
</div>
<style>
.date_group_v2 label {
display: block;
}
.date_group_v2 {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.date_group_v2 > * {
flex-grow: 1;
margin-right: 10px;
}
.date_group_v2 > *:last-child {
margin-right: 0px;
}
.date_group_shortcutes {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.date_group_shortcutes > * {
margin-right: 10px;
}
.date_group_shortcutes > *:first-child {
float: left;
}
</style>
<script>
const value_field = document.querySelector("#<?php echo $field['id'] ?>");
const container = document.querySelector("#container-<?php echo $field['id'] ?>");
const convertMS = 1000;
const get_value = () => value_field.value && value_field.value.length ? parseInt(value_field.value) * convertMS : Date.now();
const set_value = (timestamp) => {
value_field.value = Math.round(timestamp / convertMS);
};
// retrieve field value or set the field value to now
set_value(get_value());
// get current value of field
const set_date = new Date(get_value());
// grab inputs
const input_weekday = document.querySelector("#container-<?php echo $field['id'] ?> .input-weekday");
const input_month = document.querySelector("#container-<?php echo $field['id'] ?> .input-month");
const input_date = document.querySelector("#container-<?php echo $field['id'] ?> .input-date");
const input_year = document.querySelector("#container-<?php echo $field['id'] ?> .input-year");
const input_hour = document.querySelector("#container-<?php echo $field['id'] ?> .input-hour");
const input_min = document.querySelector("#container-<?php echo $field['id'] ?> .input-minute");
const input_ampm = document.querySelector("#container-<?php echo $field['id'] ?> .input-ampm");
// applies JS Date object to HTML inputs
function set_date_inputs(value) {
const hours = value.getHours();
const formatted_hours = ((hours + 11) % 12 + 1);
input_month.value = value.getMonth();
input_date.value = value.getDate();
input_year.value = value.getFullYear();
input_hour.value = formatted_hours;
input_min.value = value.getMinutes();
input_ampm.value = value.getHours() > 11 ? "pm" : "am";
const weekday = value.getDay();;
input_weekday.innerText = ["Sun", "Mon","Tues", "Wed", "Thur", "Fri", "Sat"][weekday];
set_value(value.getTime());
console.log('inputs', value.toLocaleString());
return value;
}
// pulls values from all HTML inputs and applies them
function read_value() {
let date = new Date(get_value());
let minute = parseInt(input_min.value);
const hr = parseInt(input_hour.value);
// roll hours down ?
if (hr == 0) {
const now = date.getTime() - (13 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
return;
}
// roll hours up ?
if (hr == 13) {
const now = date.getTime() + (13 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
return;
}
// roll minutes down ?
if (minute == -1) {
date.setMinutes(59);
const now = date.getTime() - (1 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
read_value(); // then hours?
return;
}
// roll minutes up ?
if (minute == 60) {
date.setMinutes(0);
const now = date.getTime() + (1 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
read_value(); // then hours?
return;
}
const dateVal = parseInt(input_date.value);
if (dateVal == 0) { // roll dates down ? (end of prev month)
date.setDate(1);
const now = date.getTime() - (24 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
read_value(); // then years?
return;
} else {
// set date
date.setDate(dateVal);
if (date.getDate() < dateVal) { // roll dates up ? (next month)
const now = date.getTime() + (24 * 60 * 60 * 1000);
const next = new Date(now);
next.setDate(1);
set_date_inputs(next);
read_value(); // then years?
return;
} else { // set month
date.setMonth(parseInt(input_month.value));
}
}
date.setMinutes(minute);
date.setFullYear(parseInt(input_year.value));
date.setHours(hr);
if (input_ampm.value == "am" && hr >= 12) {
date.setHours(0);
}
if (input_ampm.value == "pm" && hr <= 11) {
date.setHours(12 + hr);
}
const weekday = date.getDay();
input_weekday.innerText = ["Sun", "Mon","Tues", "Wed", "Thur", "Fri", "Sat"][weekday];
input_date.value = date.getDate();
set_value(date.getTime());
console.log('read', date.toLocaleString())
}
set_date_inputs(set_date);
// section for buttons: -Week -Day -Hour Now +Hour +Day +Week
document.querySelector("#container-<?php echo $field['id'] ?> .today").addEventListener("click", () => {
set_date_inputs(new Date());
});
document.querySelector("#container-<?php echo $field['id'] ?> .prev_hour").addEventListener("click", () => {
const now = get_value() - (60 * 60 * 1000);
set_date_inputs(new Date(now));
});
document.querySelector("#container-<?php echo $field['id'] ?> .next_hour").addEventListener("click", () => {
const now = get_value() + (60 * 60 * 1000);
set_date_inputs(new Date(now));
});
document.querySelector("#container-<?php echo $field['id'] ?> .prev_day").addEventListener("click", () => {
const now = get_value() - (24 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
});
document.querySelector("#container-<?php echo $field['id'] ?> .next_day").addEventListener("click", () => {
const now = get_value() + (24 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
});
document.querySelector("#container-<?php echo $field['id'] ?> .prev_week").addEventListener("click", () => {
const now = get_value() - (7 * 24 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
});
document.querySelector("#container-<?php echo $field['id'] ?> .next_week").addEventListener("click", () => {
const now = get_value() + (7 * 24 * 60 * 60 * 1000);
set_date_inputs(new Date(now));
});
// event listeners for HTML5 inputs
input_month.addEventListener("input", read_value);
input_date.addEventListener("input", read_value);
input_year.addEventListener("input", read_value);
input_hour.addEventListener("input", read_value);
input_min.addEventListener("input", read_value);
input_ampm.addEventListener("change", read_value);
for (const element of [...document.querySelectorAll(".date_group_timezone")]) {
try {
element.innerText = "Timezone: " + (Intl.DateTimeFormat().resolvedOptions().timeZone || "Uknown") + ` (UTC ${(new Date().getTimezoneOffset() / 60 * -1)})`;
} catch (e) {
element.innerText = "Timezone: Uknown (UTC ?)";
}
}
</script>
</div>
<?php
}
/*
* render_field_settings()
*
* Create extra options for your field. This is rendered when editing a field.
* The value of $field['name'] can be used (like bellow) to save extra data to the $field
*
* @type action
* @since 3.6
* @date 23/01/13
*
* @param $field - an array holding all the field's data
*/
function render_field_settings( $field ) {
// global
global $wp_locale;
// // vars
// $d_m_Y = date_i18n( 'd/m/Y g:i a' );
// $m_d_Y = date_i18n( 'm/d/Y g:i a' );
// $F_j_Y = date_i18n( 'F j, Y g:i a' );
// $Ymd = date_i18n( 'Y-m-d H:i:s' );
// // display_format
// acf_render_field_setting(
// $field,
// array(
// 'label' => __( 'Display Format', 'acf' ),
// 'instructions' => __( 'The format displayed when editing a post', 'acf' ),
// 'type' => 'radio',
// 'name' => 'display_format',
// 'other_choice' => 1,
// 'choices' => array(
// 'd/m/Y g:i a' => '<span>' . $d_m_Y . '</span><code>d/m/Y g:i a</code>',
// 'm/d/Y g:i a' => '<span>' . $m_d_Y . '</span><code>m/d/Y g:i a</code>',
// 'F j, Y g:i a' => '<span>' . $F_j_Y . '</span><code>F j, Y g:i a</code>',
// 'Y-m-d H:i:s' => '<span>' . $Ymd . '</span><code>Y-m-d H:i:s</code>',
// 'other' => '<span>' . __( 'Custom:', 'acf' ) . '</span>',
// ),
// )
// );
// // return_format
// acf_render_field_setting(
// $field,
// array(
// 'label' => __( 'Return Format', 'acf' ),
// 'instructions' => __( 'The format returned via template functions', 'acf' ),
// 'type' => 'radio',
// 'name' => 'return_format',
// 'other_choice' => 1,
// 'choices' => array(
// 'd/m/Y g:i a' => '<span>' . $d_m_Y . '</span><code>d/m/Y g:i a</code>',
// 'm/d/Y g:i a' => '<span>' . $m_d_Y . '</span><code>m/d/Y g:i a</code>',
// 'F j, Y g:i a' => '<span>' . $F_j_Y . '</span><code>F j, Y g:i a</code>',
// 'Y-m-d H:i:s' => '<span>' . $Ymd . '</span><code>Y-m-d H:i:s</code>',
// 'other' => '<span>' . __( 'Custom:', 'acf' ) . '</span>',
// ),
// )
// );
// // first_day
// acf_render_field_setting(
// $field,
// array(
// 'label' => __( 'Week Starts On', 'acf' ),
// 'instructions' => '',
// 'type' => 'select',
// 'name' => 'first_day',
// 'choices' => array_values( $wp_locale->weekday ),
// )
// );
}
/*
* format_value()
*
* This filter is appied to the $value after it is loaded from the db and before it is returned to the template
*
* @type filter
* @since 3.6
* @date 23/01/13
*
* @param $value (mixed) the value which was loaded from the database
* @param $post_id (mixed) the $post_id from which the value was loaded
* @param $field (array) the field array holding all the field options
*
* @return $value (mixed) the modified value
*/
function format_value( $value, $post_id, $field ) {
return intval($value);
}
/**
* This filter is applied to the $field after it is loaded from the database
* and ensures the return and display values are set.
*
* @type filter
* @since 5.11.0
* @date 28/09/21
*
* @param array $field The field array holding all the field options.
*
* @return array
*/
function load_field( $field ) {
// if ( empty( $field['display_format'] ) ) {
// $field['display_format'] = $this->defaults['display_format'];
// }
// if ( empty( $field['return_format'] ) ) {
// $field['return_format'] = $this->defaults['return_format'];
// }
return $field;
}
/**
* Return the schema array for the REST API.
*
* @param array $field
* @return array
*/
public function get_rest_schema( array $field ) {
return array(
'type' => array( 'string', 'null' ),
'description' => 'A `Y-m-d H:i:s` formatted date string.',
'required' => ! empty( $field['required'] ),
);
}
}
// initialize
acf_register_field_type( 'acf_field_date_and_time_picker_v2' );
endif; // class_exists check
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment