1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 08:17:12 +02:00

Major improvements to InputfieldDateTime including a significant refactoring, adding support for HTML5 date/time input types, and a new date selection input using separate selects for month, day and year

This commit is contained in:
Ryan Cramer
2020-03-06 14:04:17 -05:00
parent de5b7d9207
commit 15793931f4
10 changed files with 1566 additions and 322 deletions

View File

@@ -165,6 +165,23 @@ class PageArray extends PaginatedArray implements WirePaginatable {
public function makeBlankItem() {
return $this->wire('pages')->newPage();
}
/**
* Creates a new blank instance of this PageArray, for internal use.
*
* #pw-internal
*
* @return PageArray
*
*/
public function makeNew() {
$class = get_class($this);
/** @var PageArray $newArray */
$newArray = $this->wire(new $class());
// $newArray->finderOptions($this->finderOptions());
if($this->lazyLoad) $newArray->_lazy(true);
return $newArray;
}
/**
* Import the provided pages into this PageArray.
@@ -651,12 +668,12 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @param array $options
* @param array|null $options Specify array to set or omit this argument to get
* @return array
*
*/
public function finderOptions(array $options = array()) {
$this->finderOptions = $options;
public function finderOptions($options = null) {
if(is_array($options)) $this->finderOptions = $options;
return $this->finderOptions;
}

View File

@@ -1 +1 @@
.pw-content .ui-datepicker,#content .ui-datepicker{font-size:1.1em}.pw-content .ui-datepicker-calendar,#content .ui-datepicker-calendar{margin-top:0}.pw-content .ui-datepicker-inline,#content .ui-datepicker-inline{padding-bottom:.5em}.ui-timepicker-div .ui-widget-header,#content .ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl,#content .ui-timepicker-div dl{text-align:left;border:none;margin:0;padding:0 0 0 5px}.ui-timepicker-div dl dt,#content .ui-timepicker-div dl dt{height:25px;margin-bottom:-25px;padding-top:0;border:none;font-weight:normal}.ui-timepicker-div dl dd,#content .ui-timepicker-div dl dd{margin:0 10px 10px 65px;padding:0}.ui-timepicker-div td,#content .ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label,#content .ui-tpicker-grid-label{background:none;border:none;margin:0;padding:0}#ui-datepicker-div{font-size:12px;line-height:14px;display:none}button.ui-datepicker-trigger,a.pw-ui-datepicker-trigger{margin:0 0 0 .5em}input.InputfieldDatetimeDatepicker{position:relative;z-index:10}
.pw-content .ui-datepicker,#content .ui-datepicker{font-size:1.1em}.pw-content .ui-datepicker-calendar,#content .ui-datepicker-calendar{margin-top:0}.pw-content .ui-datepicker-inline,#content .ui-datepicker-inline{padding-bottom:.5em;font-size:14px}.ui-timepicker-div .ui-widget-header,#content .ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl,#content .ui-timepicker-div dl{text-align:left;border:none;margin:0;padding:0 0 0 5px}.ui-timepicker-div dl dt,#content .ui-timepicker-div dl dt{height:25px;margin-bottom:-25px;padding-top:0;border:none;font-weight:normal}.ui-timepicker-div dl dd,#content .ui-timepicker-div dl dd{margin:0 10px 10px 65px;padding:0}.ui-timepicker-div td,#content .ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label,#content .ui-tpicker-grid-label{background:none;border:none;margin:0;padding:0}#ui-datepicker-div{font-size:12px;line-height:14px;display:none}button.ui-datepicker-trigger,a.pw-ui-datepicker-trigger{margin:0 0 0 .5em}input.InputfieldDatetimeDatepicker{position:relative;z-index:10}input.InputfieldDatetimeDatepicker2{display:none}.InputfieldDatetime input[type=date],.InputfieldDatetime input[type=time]{width:auto}.InputfieldDatetime.InputfieldDatetimeMulti input,.InputfieldDatetime.InputfieldDatetimeMulti select{margin-bottom:4px}.InputfieldDatetime.InputfieldDatetimeMulti select{width:auto}

View File

@@ -1,4 +1,7 @@
/**
* Manages InputfieldDatetime (text) elements with jQuery UI datepickers
*
*/
function InputfieldDatetimeDatepicker($t) {
var pickerVisible = $t.is(".InputfieldDatetimeDatepicker2");
@@ -78,14 +81,112 @@ function InputfieldDatetimeDatepicker($t) {
}
/**
* Manages InputfieldDatetimeSelect elements
*
*/
function InputfieldDatetimeSelect() {
/**
* Validate selection in InputfieldDatetime selects
*
*/
function validate($select) {
var $parent = $select.parent(),
$month = $parent.children('.InputfieldDatetimeMonth'),
month = parseInt($month.val()),
$day = $parent.children('.InputfieldDatetimeDay'),
day = parseInt($day.val()),
$year = $parent.children('.InputfieldDatetimeYear'),
year = parseInt($year.val()),
$value = $parent.children('.InputfieldDatetimeValue'),
date = month && day && year ? new Date(year, month - 1, day) : null,
errorClass = 'InputfieldDatetimeError';
if(date && date.getMonth() + 1 != month) {
// day not valid for month
day = '';
$day.val('').addClass(errorClass);
} else {
$day.removeClass(errorClass);
}
$value.val(date && day ? year + '-' + month + '-' + day : '');
}
/**
* Called when the Year select has changed in an InputfieldDatetimeSelect
*
* Enables addition of years before/after when "-" or "+" option is selected
*
*/
function yearChange($select) {
var value = $select.val();
if(value !== '-' && value !== '+') return;
var $blankOption = $select.find('option[value=""]'),
$option = $select.find('option[value="' + value + '"]'),
fromYear = parseInt($select.attr('data-from-year')),
toYear = parseInt($select.attr('data-to-year')),
numYears = toYear - fromYear,
n = 0,
$o;
if(numYears < 10) numYears = 10;
if(value === '-') {
// add # years prior
toYear = fromYear-1;
fromYear = fromYear - numYears;
for(n = toYear; n >= fromYear; n--) {
$o = jQuery('<option />').val(n).text(n);
$select.prepend($o);
}
$option.html('&lt; ' + fromYear);
$select.prepend($option).prepend($blankOption);
$select.val(toYear);
$select.attr('data-from-year', fromYear);
} else if(value === '+') {
// add # years after
fromYear = toYear+1;
toYear += numYears;
for(n = fromYear; n <= toYear; n++) {
$o = $('<option />').val(n).text(n);
$select.append($o);
}
$option.html('&gt; ' + toYear);
$select.append($option);
$select.val(fromYear);
$select.attr('data-to-year', toYear);
}
}
jQuery(document).on('change', '.InputfieldDatetimeSelect select', function() {
var $select = jQuery(this);
if($select.hasClass('InputfieldDatetimeYear')) yearChange($select);
validate($select);
});
}
/**
* Document ready
*
*/
jQuery(document).ready(function($) {
// init datepickers present when document is ready
$("input.InputfieldDatetimeDatepicker:not(.InputfieldDatetimeDatepicker3):not(.initDatepicker)").each(function(n) {
InputfieldDatetimeDatepicker($(this));
});
// init datepicker that should appear on focus (3) of text input, that wasn't present at document.ready
$(document).on('focus', 'input.InputfieldDatetimeDatepicker3:not(.hasDatepicker)', function() {
InputfieldDatetimeDatepicker($(this));
});
// init date selects
InputfieldDatetimeSelect();
});

View File

@@ -1 +1 @@
function InputfieldDatetimeDatepicker(h){var i=h.is(".InputfieldDatetimeDatepicker2");var k=parseInt(h.attr("data-ts"));var d=null;var b=h.attr("data-dateformat");var g=h.attr("data-timeformat");var f=parseInt(h.attr("data-timeselect"));var j=g.length>0&&!i;var m=h.is(".InputfieldDatetimeDatepicker3")?"focus":"button";var l=parseInt(h.attr("data-ampm"))>0;var o=h.attr("data-yearrange");if(k>1){d=new Date(k)}if(i){var c=$("<div></div>");h.after(c)}else{var c=h}var n={changeMonth:true,changeYear:true,showOn:m,buttonText:"&gt;",showAnim:"fadeIn",dateFormat:b,gotoCurrent:true,defaultDate:d};if(o&&o.length){n.yearRange=o}if(j){n.ampm=l;n.timeFormat=g;if(f>0){n.controlType="select";n.oneLine=true}if(g.indexOf("ss")>-1){n.showSecond=true}if(g.indexOf("m")==-1){n.showMinute=false}c.datetimepicker(n)}else{c.datepicker(n)}if(i){c.change(function(p){var r=c.datepicker("getDate");var q=$.datepicker.formatDate(b,r);h.val(q)})}if(m=="button"){var a=h.next("button.ui-datepicker-trigger");if(a.length){var e=$("<a class='pw-ui-datepicker-trigger' href='#'><i class='fa fa-calendar'></i></a>");a.after(e).hide();e.click(function(){a.click();return false})}}h.addClass("initDatepicker")}jQuery(document).ready(function(a){a("input.InputfieldDatetimeDatepicker:not(.InputfieldDatetimeDatepicker3):not(.initDatepicker)").each(function(b){InputfieldDatetimeDatepicker(a(this))});a(document).on("focus","input.InputfieldDatetimeDatepicker3:not(.hasDatepicker)",function(){InputfieldDatetimeDatepicker(a(this))})});
function InputfieldDatetimeDatepicker($t){var pickerVisible=$t.is(".InputfieldDatetimeDatepicker2");var ts=parseInt($t.attr("data-ts"));var tsDate=null;var dateFormat=$t.attr("data-dateformat");var timeFormat=$t.attr("data-timeformat");var timeSelect=parseInt($t.attr("data-timeselect"));var hasTimePicker=timeFormat.length>0&&!pickerVisible;var showOn=$t.is(".InputfieldDatetimeDatepicker3")?"focus":"button";var ampm=parseInt($t.attr("data-ampm"))>0;var yearRange=$t.attr("data-yearrange");if(ts>1)tsDate=new Date(ts);if(pickerVisible){var $datepicker=$("<div></div>");$t.after($datepicker)}else{var $datepicker=$t}var options={changeMonth:true,changeYear:true,showOn:showOn,buttonText:"&gt;",showAnim:"fadeIn",dateFormat:dateFormat,gotoCurrent:true,defaultDate:tsDate};if(yearRange&&yearRange.length)options.yearRange=yearRange;if(hasTimePicker){options.ampm=ampm;options.timeFormat=timeFormat;if(timeSelect>0){options.controlType="select";options.oneLine=true}if(timeFormat.indexOf("ss")>-1)options.showSecond=true;if(timeFormat.indexOf("m")==-1)options.showMinute=false;$datepicker.datetimepicker(options)}else{$datepicker.datepicker(options)}if(pickerVisible){$datepicker.change(function(e){var d=$datepicker.datepicker("getDate");var str=$.datepicker.formatDate(dateFormat,d);$t.val(str)})}if(showOn=="button"){var $button=$t.next("button.ui-datepicker-trigger");if($button.length){var $a=$("<a class='pw-ui-datepicker-trigger' href='#'><i class='fa fa-calendar'></i></a>");$button.after($a).hide();$a.click(function(){$button.click();return false})}}$t.addClass("initDatepicker")}function InputfieldDatetimeSelect(){function validate($select){var $parent=$select.parent(),$month=$parent.children(".InputfieldDatetimeMonth"),month=parseInt($month.val()),$day=$parent.children(".InputfieldDatetimeDay"),day=parseInt($day.val()),$year=$parent.children(".InputfieldDatetimeYear"),year=parseInt($year.val()),$value=$parent.children(".InputfieldDatetimeValue"),date=month&&day&&year?new Date(year,month-1,day):null,errorClass="InputfieldDatetimeError";if(date&&date.getMonth()+1!=month){day="";$day.val("").addClass(errorClass)}else{$day.removeClass(errorClass)}$value.val(date&&day?year+"-"+month+"-"+day:"")}function yearChange($select){var value=$select.val();if(value!=="-"&&value!=="+")return;var $blankOption=$select.find('option[value=""]'),$option=$select.find('option[value="'+value+'"]'),fromYear=parseInt($select.attr("data-from-year")),toYear=parseInt($select.attr("data-to-year")),numYears=toYear-fromYear,n=0,$o;if(numYears<10)numYears=10;if(value==="-"){toYear=fromYear-1;fromYear=fromYear-numYears;for(n=toYear;n>=fromYear;n--){$o=jQuery("<option />").val(n).text(n);$select.prepend($o)}$option.html("&lt; "+fromYear);$select.prepend($option).prepend($blankOption);$select.val(toYear);$select.attr("data-from-year",fromYear)}else if(value==="+"){fromYear=toYear+1;toYear+=numYears;for(n=fromYear;n<=toYear;n++){$o=$("<option />").val(n).text(n);$select.append($o)}$option.html("&gt; "+toYear);$select.append($option);$select.val(fromYear);$select.attr("data-to-year",toYear)}}jQuery(document).on("change",".InputfieldDatetimeSelect select",function(){var $select=jQuery(this);if($select.hasClass("InputfieldDatetimeYear"))yearChange($select);validate($select)})}jQuery(document).ready(function($){$("input.InputfieldDatetimeDatepicker:not(.InputfieldDatetimeDatepicker3):not(.initDatepicker)").each(function(n){InputfieldDatetimeDatepicker($(this))});$(document).on("focus","input.InputfieldDatetimeDatepicker3:not(.hasDatepicker)",function(){InputfieldDatetimeDatepicker($(this))});InputfieldDatetimeSelect()});

View File

@@ -8,63 +8,247 @@
* For documentation about the fields used in this class, please see:
* /wire/core/Fieldtype.php
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com
*
* @property string $dateInputFormat
* @property String $timeInputFormat
* @property int $timeInputSelect
* @property int $datepicker
* @property string $yearRange
* @property int|bool $defaultToday
*
* ~~~~~~
* // get a datetime Inputfield
* $f = $modules->get('InputfieldDatetime');
* $f->attr('name', 'test_date');
* $f->label = 'Test date';
* $f->val(time()); // value is get or set a UNIX timestamp
*
* // date input with jQuery UI datepicker on focus
* $f->inputType = 'text'; // not necessary as this is the default
* $f->datepicker = InputfieldDatetime::datepickerFocus;
*
* // date selects
* $f->inputType = 'select';
* $f->dateSelectFormat = 'mdy'; // month abbr (i.e. 'Sep'), day, year
* $f->dateSelectFormat = 'Mdy'; // month full (i.e. 'September'), day, year
* $f->yearFrom = 2019; // optional year range from
* $f->yearTo = 2024; // optional year range to
*
* // HTML5 date, time or date+time inputs
* $f->inputType = 'html';
* $f->htmlType = 'date'; // or 'time' or 'datetime'
* ~~~~~~
*
* @property int $value This Inputfield keeps the value in UNIX timestamp format (int).
* @property string $inputType Input type to use, one of: "text", "select" or "html" (when html type is used, also specify $htmlType).
* @property int|bool $defaultToday When no value is present, default to todays date/time?
* @property int $subYear Substitute year when month+day or time only selections are made (default=2010)
* @property int $subDay Substitute day when month+year or time only selectinos are made (default=8)
* @property int $subMonth Substitute month when time-only selections are made (default=4)
* @property int $subHour Substitute hour when date-only selections are made (default=0)
* @property int $subMinute Substitute minute when date-only selection are made (default=0)
* @property bool|int $requiredAttr When combined with "required" option, this also makes it use the HTML5 "required" attribute (default=false).
*
* Properties specific to "text" input type (with optional jQuery UI datepicker)
* =============================================================================
* @property int $datepicker jQuery UI datepicker type (see `datepicker*` constants)
* @property string $yearRange Selectable year range in the format `-30:+20` where -30 is number of years before now and +20 is number of years after now.
* @property int $timeInputSelect jQuery UI timeSelect type (requires datepicker)—specify 1 to use a `<select>` for time input, or 0 to use a slider (default=0)
* @property string $dateInputFormat Date input format to use, see WireDateTime::$dateFormats (default='Y-m-d')
* @property string $timeInputFormat Time input format to use, see WireDateTime::$timeFormats (default='')
*
* Properties specific to "html" input type
* ========================================
* @property string $htmlType When "html" is selection for $inputType, this should be one of: "date", "time" or "datetime".
* @property int $timeStep Refers to the step attribute on time inputs
* @property string $timeMin Refers to the min attribute on time inputs (HH:MM)
* @property string $timeMax Refers to the max attribute on time inputs (HH:MM)
* @property int $dateStep Refers to the step attribute on date inputs
* @property string $dateMin Refers to the min attribute on date inputs, ISO-8601 (YYYY-MM-DD)
* @property string $dateMax Refers to the max attribute on date inputs, ISO-8601 (YYYY-MM-DD)
*
* Properties specific to "select" input type
* ==========================================
* @property string $dateSelectFormat Format to use for date select
* @property string $timeSelectFormat Format to use for time select
* @property int $yearFrom First selectable year (default=current year - 100)
* @property int $yearTo Last selectable year (default=current year + 20)
* @property bool|int $yearLock Disallow selection of years outside the yearFrom/yearTo range? (default=false)
*
*
*/
class InputfieldDatetime extends Inputfield {
const defaultDateInputFormat = 'Y-m-d';
const datepickerNo = 0; // no datepicker
const datepickerClick = 1; // datepicker on click
const datepickerInline = 2; // inline datepicker, always visible (no timepicker support)
const datepickerFocus = 3; // datepicker on field focus
public static function getModuleInfo() {
return array(
'title' => __('Datetime', __FILE__), // Module Title
'summary' => __('Inputfield that accepts date and optionally time', __FILE__), // Module Summary
'version' => 106,
'permanent' => true,
);
'version' => 107,
'permanent' => true,
);
}
/**
* ISO-8601 date/time formats (default date input format)
*
* #pw-internal
*
*/
const defaultDateInputFormat = 'Y-m-d';
const defaultTimeInputFormat = 'H:i';
const secondsTimeInputFormat = 'H:i:s';
/**
* jQuery UI datepicker: None
*
*/
const datepickerNo = 0;
/**
* jQuery UI datepicker: Click button to show
*
*/
const datepickerClick = 1;
/**
* jQuery UI datepicker: Inline datepicker always visible (no timepicker support)
*
*/
const datepickerInline = 2;
/**
* jQuery UI datepicker: Show when input focused (recommend option when using datepicker)
*
*/
const datepickerFocus = 3;
/**
* @var InputfieldDatetimeType[]
*
*/
static protected $inputTypes = array();
/**
* Initialize the date/time inputfield
*
*/
public function init() {
$this->attr('type', 'text');
$this->attr('size', 25);
$this->attr('placeholder', '');
$this->set('dateInputFormat', self::defaultDateInputFormat);
$this->set('timeInputFormat', '');
$this->set('timeInputSelect', 0);
$this->set('datepicker', self::datepickerNo);
$this->set('yearRange', '');
$this->set('defaultToday', 0);
if($this->languages) foreach($this->languages as $language) {
/** @var Language $language */
// account for alternate formats in other languages
if($language->isDefault()) continue;
$this->set("dateInputFormat$language", '');
$this->set("timeInputFormat$language", '');
$this->set('inputType', 'text');
$this->set('subYear', 2010);
$this->set('subMonth', 4);
$this->set('subDay', 8);
$this->set('subHour', 0);
$this->set('subMinute', 0);
$this->set('requiredAttr', 0);
foreach($this->getInputTypes() as $name => $type) {
$this->setArray($type->getDefaultSettings());
}
parent::init();
}
/**
* Return ISO-8601 substitute date (combination of subYear, subMonth, subDay)
*
* #pw-internal
*
* @return string
*
*/
public function subDate() {
$year = (int) parent::getSetting('subYear');
$month = (int) parent::getSetting('subMonth');
$day = (int) parent::getSetting('subDay');
if($year < 1000 || $year > 2500) $year = (int) date('Y');
if($month > 12 || $month < 1) $month = 1;
if($month < 10) $month = "0$month";
if($day > 31 || $day < 1) $day = 1;
if($day < 10) $day = "0$day";
return "$year-$month-$day";
}
/**
* Return ISO-8601 substitute time (combination of subHour:subMinute:00)
*
* #pw-internal
*
* @return string
*
*/
public function subTime() {
$hour = (int) parent::getSetting('subHour');
$minute = (int) parent::getSetting('subMinute');
if($hour > 23 || $hour < 0) $hour = 0;
if($hour < 10) $hour = "0$hour";
if($minute > 59 || $minute < 0) $minute = 0;
if($minute < 10) $minute = "0$minute";
return "$hour:$minute:00";
}
/**
* Get all date/time input types
*
* @return InputfieldDatetimeType[]
*
*/
public function getInputTypes() {
if(count(self::$inputTypes)) return self::$inputTypes;
$path = dirname(__FILE__) . '/';
require_once($path . 'InputfieldDatetimeType.php');
$dir = new \DirectoryIterator($path . 'types/');
foreach($dir as $file) {
if($file->isDir() || $file->isDot() || $file->getExtension() != 'php') continue;
require_once($file->getPathname());
$className = wireClassName($file->getBasename('.php'), true);
/** @var InputfieldDatetimeType $type */
$type = $this->wire(new $className($this));
$name = $type->getTypeName();
self::$inputTypes[$name] = $type;
}
return self::$inputTypes;
}
/**
* Get current date/time input type instance
*
* @param string $typeName
* @return InputfieldDatetimeType
*
*/
public function getInputType($typeName = '') {
$inputTypes = $this->getInputTypes();
if(!$typeName) $typeName = $this->inputType;
if(!$typeName || !isset($inputTypes[$typeName])) $typeName = 'text';
return $inputTypes[$typeName];
}
/**
* Set property
*
* @param string $key
* @param mixed $value
* @return Inputfield|WireData
*
*/
public function set($key, $value) {
if($key === 'dateMin' || $key === 'dateMax') {
if(is_int($value)) $value = date(self::defaultDateInputFormat, $value);
} else if($key === 'timeMin' || $key === 'timeMax') {
if(is_int($value)) $value = date(self::defaultTimeInputFormat, $value);
}
return parent::set($key, $value);
}
/**
* Called before the render method, from a hook in the Inputfield class
*
@@ -73,59 +257,13 @@ class InputfieldDatetime extends Inputfield {
*
* @param Inputfield $parent
* @param bool $renderValueMode
* @return $this
* @return bool
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
$this->addClass('InputfieldNoFocus', 'wrapClass');
list($dateFormat, $timeFormat) = $this->getInputFormats();
if($dateFormat) {}
$useTime = false;
$language = $this->wire('languages') ? $this->wire('user')->language : null;
if($this->datepicker) {
$this->wire('modules')->get('JqueryCore'); // Jquery Core required before Jquery UI
$this->wire('modules')->get('JqueryUI');
$this->addClass("InputfieldDatetimeDatepicker InputfieldDatetimeDatepicker{$this->datepicker}");
if(strlen($timeFormat) && $this->datepicker != self::datepickerInline) {
// add in the timepicker script, if applicable
$useTime = true;
$url = $this->config->urls->get('InputfieldDatetime');
$this->config->scripts->add($url . 'timepicker/jquery-ui-timepicker-addon.min.js');
$this->config->styles->add($url . 'timepicker/jquery-ui-timepicker-addon.min.css');
}
if($language) {
// include i18n support for the datepicker
// note that the 'xx' in the filename is just a placeholder to indicate what should be replaced for translations, as that file doesn't exist
$langFile = ltrim($this->_('/wire/modules/Jquery/JqueryUI/i18n/jquery.ui.datepicker-xx.js'), '/'); // Datepicker translation file // Replace 'xx' with jQuery UI language code or specify your own js file
if(is_file($this->config->paths->root . $langFile)) {
// add a custom language file
$this->config->scripts->add($this->config->urls->root . $langFile);
} else {
// attempt to auto-find one based on the language name (which are often 2 char language codes)
$langFile = "wire/modules/Jquery/JqueryUI/i18n/jquery.ui.datepicker-{$language->name}.js";
if(is_file($this->config->paths->root . $langFile)) $this->config->scripts->add($this->config->urls->root . $langFile);
}
if($useTime) {
$langFile = $this->_('timepicker/i18n/jquery-ui-timepicker-xx.js'); // Timepicker translation file // Replace 'xx' with jQuery UI language code or specify your own js file. Timepicker i18n files are located in /wire/modules/Inputfield/InputfieldDatetime/timepicker/i18n/.
$path = $this->config->paths->get('InputfieldDatetime');
$url = $this->config->urls->get('InputfieldDatetime');
if(is_file($path . $langFile)) {
// add a custom language file
$this->config->scripts->add($url . $langFile);
} else {
// attempt to auto-find one based on the language name (which are often 2 char language codes)
$langFile = str_replace('-xx.', "-$language->name.", $langFile);
if(is_file($path . $langFile)) {
$this->config->scripts->add($url . $langFile);
}
}
}
}
}
parent::renderReady($parent, $renderValueMode);
$this->addClass("InputfieldNoFocus", 'wrapClass');
$this->getInputType()->renderReady();
return parent::renderReady($parent, $renderValueMode);
}
/**
@@ -135,56 +273,7 @@ class InputfieldDatetime extends Inputfield {
*
*/
public function ___render() {
$sanitizer = $this->wire('sanitizer');
$datetime = $this->wire('datetime');
list($dateFormat, $timeFormat) = $this->getInputFormats();
$useTime = false;
if(strlen($timeFormat) && $this->datepicker && $this->datepicker != self::datepickerInline) $useTime = true;
$attrs = $this->getAttributes();
$value = $attrs['value'];
$valueTS = (int) $value*1000; // TS=for datepicker/javascript, which uses milliseconds rather than seconds
unset($attrs['value']);
if(!$value && $this->defaultToday) {
$value = date($dateFormat);
if($timeFormat) $value .= ' ' . date($timeFormat);
$valueTS = time()*1000;
} else if($value) {
$value = trim(date($dateFormat . ' ' . $timeFormat, (int) $value));
}
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$dateFormatJS = $sanitizer->entities($datetime->convertDateFormat($dateFormat, 'js'));
$timeFormatJS = $useTime ? $datetime->convertDateFormat($timeFormat, 'js') : '';
if(strpos($timeFormatJS, 'h24') !== false) {
// 24 hour format
$timeFormatJS = str_replace(array('hh24', 'h24'), array('HH', 'H'), $timeFormatJS);
$ampm = 0;
} else {
$ampm = 1;
}
if(strlen($timeFormatJS)) $timeFormatJS = $sanitizer->entities($timeFormatJS);
if(empty($value)) $value = '';
$yearRange = $sanitizer->entities($this->yearRange);
$out =
"<input " . $this->getAttributesString($attrs) . " " .
"value='$value' " .
"data-dateformat='$dateFormatJS' " .
"data-timeformat='$timeFormatJS' " .
"data-timeselect='$this->timeInputSelect' " .
"data-ts='$valueTS' " .
"data-ampm='$ampm' " .
(strlen($yearRange) ? "data-yearrange='$yearRange' " : '') .
"/>";
return $out;
return $this->getInputType()->render();
}
/**
@@ -192,63 +281,77 @@ class InputfieldDatetime extends Inputfield {
*
*/
public function ___renderValue() {
$value = $this->attr('value');
$out = $this->getInputType()->renderValue();
if($out) return $out;
$value = $this->attr('value');
if(!$value) return '';
$format = trim($this->dateInputFormat . ' ' . $this->timeInputFormat);
$format = self::defaultDateInputFormat . ' ';
if($this->timeStep > 0 && $this->timeStep < 60) {
$format .= self::secondsTimeInputFormat;
} else {
$format .= self::defaultTimeInputFormat;
}
return $this->wire('datetime')->formatDate($value, trim($format));
}
/**
* Process input
*
* @param WireInputData $input
* @return Inputfield|InputfieldDatetime
*
*/
public function ___processInput(WireInputData $input) {
$valuePrevious = $this->val();
$value = $this->getInputType()->processInput($input);
if($value === false) {
// false indicates type is not processing input
parent::___processInput($input);
$value = $this->getAttribute('value');
} else {
$this->setAttribute('value', $value);
}
if($value !== $valuePrevious) {
$this->trackChange('value', $valuePrevious, $value);
$parent = $this->getParent();
if($parent) $parent->trackChange($this->name);
}
return $this;
}
/**
* Capture setting of the 'value' attribute and convert string dates to unix timestamp
*
* @param string $key
* @param mixed $value
* @return $this
* @return Inputfield|InputfieldDatetime
*
*/
public function setAttribute($key, $value) {
if($key == 'value') {
$value = $this->wire('datetime')->stringToTimestamp($value, $this->getInputFormats(true));
if($key === 'value') {
if(empty($value) && "$value" !== "0") {
// empty value thats not 0
$value = '';
} else if(is_int($value) || ctype_digit("$value")) {
// unix timestamp
$value = (int) $value;
} else if(strlen($value) > 8 && $value[4] === '-' && $value[7] === '-' && ctype_digit(substr($value, 0, 4))) {
// ISO-8601, i.e. 2010-04-08 02:48:00
$value = strtotime($value);
} else {
$value = $this->getInputType()->sanitizeValue($value);
}
}
return parent::setAttribute($key, $value);
}
/**
* Get the input format string for the user's language
*
* thanks to @oliverwehn (#1463)
*
* @param bool $getString Specify true to get a format string rather than an array
* @return array|string of dateInputFormat timeInputFormat
*
*/
protected function getInputFormats($getString = false) {
$inputFormats = array();
$language = $this->wire('user')->language;
$useLanguages = $this->wire('languages') && $language && !$language->isDefault();
foreach(array('date', 'time') as $type) {
$inputFormat = '';
if($useLanguages) {
$inputFormat = trim($this->getSetting("{$type}InputFormat{$language->id}"));
}
if(!strlen($inputFormat)) {
// fallback to default language
$inputFormat = $this->get("{$type}InputFormat");
}
$inputFormats[] = $inputFormat;
}
if($getString) return trim(implode(' ', $inputFormats));
return $inputFormats;
}
/**
* Date/time Inputfield configuration, per field
*
@@ -256,161 +359,44 @@ class InputfieldDatetime extends Inputfield {
public function ___getConfigInputfields() {
$inputfields = parent::___getConfigInputfields();
$languages = $this->wire('languages');
$datetime = $this->wire('datetime');
/** @var InputfieldInteger $f */
$f = $this->modules->get('InputfieldInteger');
$f->setAttribute('name', 'size');
$f->label = $this->_('Size');
$f->attr('value', $this->attr('size'));
$f->attr('size', 4);
$f->description = $this->_('The displayed width of this field (in characters).');
$inputfields->append($f);
$inputTypes = $this->getInputTypes();
$modules = $this->wire('modules'); /** @var Modules $modules */
/** @var InputfieldRadios $f */
$f= $this->modules->get('InputfieldRadios');
$f->label = $this->_('Date Picker');
$f->setAttribute('name', 'datepicker');
$f->addOption(self::datepickerNo, $this->_('No date/time picker'));
$f->addOption(self::datepickerFocus, $this->_('Date/time picker on field focus') . ' ' .
$this->_('(recommended)'));
$f->addOption(self::datepickerClick, $this->_('Date/time picker on button click'));
// @todo this datepickerInline option displays a datepicker that is too large, not fully styled
$f->addOption(self::datepickerInline, $this->_('Inline date picker always visible (no time picker)'));
$f->attr('value', (int) $this->datepicker);
$inputfields->append($f);
/** @var InputfieldFieldset $fieldset */
$fieldset = $this->modules->get('InputfieldFieldset');
$fieldset->label = $this->_('Date/Time Input Formats');
/** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', '_dateInputFormat');
$f->label = $this->_('Date Input Format');
$f->description = $this->_('Select the format to be used for user input to this field. Your selection will populate the field below this, which you may customize further if needed.');
$f = $modules->get('InputfieldRadios');
$f->attr('name', 'inputType');
$f->label = $this->_('Input Type');
$f->icon = 'calendar';
//$f->addOption('', $this->_('None'));
$date = strtotime('2016-04-08 5:10:02 PM');
foreach($datetime->getDateFormats() as $format) {
$dateFormatted = $datetime->formatDate($date, $format);
if($format == 'U') $dateFormatted .= " " . $this->_('(unix timestamp)');
$f->addOption($format, $dateFormatted);
if($this->dateInputFormat == $format) $f->attr('value', $format);
foreach($inputTypes as $inputTypeName => $inputType) {
$f->addOption($inputTypeName, $inputType->getTypeLabel());
}
$f->attr('onchange', "$('#Inputfield_dateInputFormat').val($(this).val());");
$fieldset->add($f);
$inputTypeVal = $this->getSetting('inputType');
if(!$inputTypeVal) $inputTypeVal = 'text';
if(!isset($inputTypes[$inputTypeVal])) $inputTypeVal = 'text';
$f->val($inputTypeVal);
$inputfields->add($f);
/** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', '_timeInputFormat');
$f->label = $this->_('Time Input Format');
$f->addOption('', $this->_('None'));
$f->description = $this->_('Select an optional time format to be used for input. If used, the calendar option will include a time picker.');
$f->icon = 'clock-o';
foreach($datetime->getTimeFormats() as $format) {
if(strpos($format, '!') === 0) continue; // skip relative formats
$timeFormatted = $datetime->formatDate($date, $format);
$f->addOption($format, $timeFormatted);
if($this->timeInputFormat == $format) $f->attr('value', $format);
foreach($inputTypes as $inputTypeName => $inputType) {
/** @var InputfieldFieldset $inputfields */
$fieldset = $modules->get('InputfieldFieldset');
$fieldset->attr('name', '_' . $inputTypeName . 'Options');
$fieldset->label = $inputType->getTypeLabel();
$fieldset->showIf = 'inputType=' . $inputTypeName;
$inputType->getConfigInputfields($fieldset);
$inputfields->add($fieldset);
}
$f->attr('onchange', "$('#Inputfield_timeInputFormat').val($(this).val());");
$f->collapsed = Inputfield::collapsedBlank;
$f->columnWidth = 50;
$fieldset->add($f);
/** @var InputfieldRadios $f */
$f = $this->modules->get("InputfieldRadios");
$f->attr('name', 'timeInputSelect');
$f->label = $this->_('Time Input Type');
$f->description = $this->_('Sliders (default) let the user slide controls to choose the time, where as Select lets the user select the time from a drop-down select.');
$f->icon = 'clock-o';
$f->addOption(0, $this->_('Sliders'));
$f->addOption(1, $this->_('Select'));
$f->optionColumns = 1;
$f->columnWidth = 50;
$f->showIf = "_timeInputFormat!='', datepicker!=" . self::datepickerNo;
$f->attr('value', $this->timeInputSelect);
$fieldset->add($f);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'dateInputFormat');
$f->attr('value', $this->dateInputFormat ? $this->dateInputFormat : self::defaultDateInputFormat);
$f->attr('size', 20);
$f->label = $this->_('Date Input Format Code');
$f->description = $this->_('This is automatically built from the date select above, unless you modify it.');
$f->icon = 'calendar';
$notes = $this->_('See the [PHP date](http://www.php.net/manual/en/function.date.php) function reference for more information on how to customize these formats.');
if($languages) $notes .= "\n" . $this->_('You may optionally specify formats for other languages here as well. Any languages left blank will inherit the default setting.');
$f->notes = $notes;
$f->collapsed = Inputfield::collapsedYes;
$f1 = $f;
$fieldset->add($f);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'timeInputFormat');
$f->attr('value', $this->timeInputFormat ? $this->timeInputFormat : '');
$f->attr('size', 20);
$f->label = $this->_('Time Input Format Code');
$f->description = $this->_('This is automatically built from the time select above, unless you modify it.');
$f->icon = 'clock-o';
$f->notes = $notes;
$f->collapsed = Inputfield::collapsedYes;
$f2 = $f;
if($languages) {
$f1->useLanguages = true;
$f2->useLanguages = true;
foreach($languages as $language) {
if($language->isDefault()) continue;
$f1->set("value$language", (string) $this->get("dateInputFormat$language"));
$f2->set("value$language", (string) $this->get("timeInputFormat$language"));
}
}
$fieldset->add($f1);
$fieldset->add($f2);
$inputfields->add($fieldset);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'yearRange');
$f->attr('value', $this->yearRange);
$f->attr('size', 10);
$f->label = $this->_('Date Picker Year Range');
$f->description = $this->_('When the date picker is used, it has a selectable year range minus and plus 10 years from the current year. To extend or reduce that, specify the quantity of years before and after [current year] in this format: "-30:+20", which would show 30 years before now and 20 years after now.');
$f->notes = $this->_('Default when no value present is "-10:+10" which shows a date picker year range 10 years before now, and 10 years after now.');
$f->icon = 'arrows-h';
$f->collapsed = Inputfield::collapsedBlank;
$f->showIf = 'datepicker!=' . self::datepickerNo;
$inputfields->append($f);
/** @var InputfieldCheckbox $f */
$f = $this->modules->get('InputfieldCheckbox');
$f->setAttribute('name', 'defaultToday');
$f->attr('value', 1);
if($this->defaultToday) $f->attr('checked', 'checked');
$f->label = $this->_("Default to today's date?");
$f->description = $this->_("If checked, this field will hold the current date when no value is entered."); // Default today description
$f->columnWidth = 50;
$f->setAttribute('name', 'defaultToday');
$f->attr('value', 1);
if($this->defaultToday) $f->attr('checked', 'checked');
$f->label = $this->_('Default to todays date?');
$f->description = $this->_('If checked, this field will hold the current date when no value is entered.'); // Default today description
$inputfields->append($f);
/** @var InputfieldText $field */
$field = $this->modules->get('InputfieldText');
$field->setAttribute('name', 'placeholder');
$field->label = $this->_('Placeholder Text');
$field->setAttribute('value', $this->attr('placeholder'));
$field->description = $this->_('Optional placeholder text that appears in the field when blank.');
$field->columnWidth = 50;
$inputfields->append($field);
return $inputfields;
}
}

View File

@@ -12,6 +12,7 @@
}
.ui-datepicker-inline {
padding-bottom: 0.5em;
font-size: 14px;
}
}
@@ -69,3 +70,24 @@ input.InputfieldDatetimeDatepicker {
z-index: 10;
}
input.InputfieldDatetimeDatepicker2 {
// inline datepicker, do not show <input> element
display: none;
}
.InputfieldDatetime {
input[type=date],
input[type=time] {
width: auto;
}
}
.InputfieldDatetime.InputfieldDatetimeMulti {
// date and time represented by separate (multiple) inputs
input, select {
margin-bottom: 4px; // for when they stack
}
select {
width: auto;
}
}

View File

@@ -0,0 +1,146 @@
<?php namespace ProcessWire;
abstract class InputfieldDatetimeType extends WireData {
/**
* @var InputfieldDatetime
*
*/
protected $inputfield;
/**
* Construct
*
* @param InputfieldDatetime $inputfield
*
*/
public function __construct(InputfieldDatetime $inputfield) {
$this->inputfield = $inputfield;
parent::__construct();
}
/**
* Get name for this type
*
* @return string
*
*/
public function getTypeName() {
return strtolower(str_replace('InputfieldDatetime', '', $this->className()));
}
/**
* Get type label
*
* @return string
*
*/
public function getTypeLabel() {
return str_replace('InputfieldDatetime', '', $this->className());
}
/**
* Get attribute
*
* @param string $key
* @return string|null
*
*/
public function getAttribute($key) {
return $this->inputfield->getAttribute($key);
}
/**
* Get attribute
*
* @param string $key
* @param string $value
* @return self
*
*/
public function setAttribute($key, $value) {
$this->inputfield->setAttribute($key, $value);
return $this;
}
/**
* Get setting
*
* @param string $key
* @return mixed
*
*/
public function getSetting($key) {
return $this->inputfield->getSetting($key);
}
/**
* Get setting or attribute or API var
*
* @param string $key
* @return mixed|null
*
*/
public function get($key) {
return $this->inputfield->get($key);
}
/**
* Get array of default settings
*
* @return array
*
*/
public function getDefaultSettings() {
return array();
}
/**
* @return string
*
*/
public function renderValue() {
return '';
}
/**
* Sanitize value to unix timestamp integer or blank string (to represent no value)
*
* @param string|int $value
* @return int|string
*
*/
public function sanitizeValue($value) {
if(is_int($value) || ctype_digit("$value")) return (int) $value;
if(empty($value)) return '';
return strtotime($value);
}
/**
* Render ready
*
*/
abstract public function renderReady();
/**
* @return string
*
*/
abstract public function render();
/**
* Process input
*
* @param WireInputData $input
* @return int|string|bool Int for UNIX timestamp date, blank string for no date, or boolean false if InputfieldDatetime should process input
*
*/
abstract public function processInput(WireInputData $input);
/**
* @param InputfieldWrapper $inputfields
*
*/
abstract public function getConfigInputfields(InputfieldWrapper $inputfields);
}

View File

@@ -0,0 +1,264 @@
<?php namespace ProcessWire;
/**
* HTML5 date/time input types
*
*/
class InputfieldDatetimeHtml extends InputfieldDatetimeType {
public function getDefaultSettings() {
return array(
'htmlType' => 'date',
'dateStep' => 0,
'dateMin' => '',
'dateMax' => '',
'timeStep' => 0,
'timeMin' => '',
'timeMax' => '',
);
}
public function getTypeLabel() {
return $this->_('HTML5 browser native date, time or both');
}
/**
* Render ready
*
*/
public function renderReady() {
if($this->getSetting('htmlType') === 'datetime') {
$this->inputfield->addClass('InputfieldDatetimeMulti', 'wrapClass'); // multiple unputs
}
}
/**
* @return string
*
*/
public function render() {
$out = '';
switch($this->getSetting('htmlType')) {
case 'date': $out = $this->renderDate(); break;
case 'time': $out = $this->renderTime(); break;
case 'datetime': $out = $this->renderDate() . '&nbsp;' . $this->renderTime(); break;
}
return $out;
}
/**
* Render date input
*
* @return string
*
*/
protected function renderDate() {
$format = InputfieldDatetime::defaultDateInputFormat;
$dateStep = (int) $this->getSetting('dateStep');
$attrs = $this->inputfield->getAttributes();
unset($attrs['size']);
$value = $attrs['value'];
if(!$value && $this->getSetting('defaultToday')) $value = time();
$value = $value ? date($format, $value) : '';
$attrs['type'] = 'date';
$attrs['value'] = $value;
$attrs['placeholder'] = 'yyyy-mm-dd'; // placeholder and pattern...
$attrs['pattern'] = '[0-9]{4}-[0-9]{2}-[0-9]{2}'; // ...used only if browser does not support HTML5 date
if($dateStep > 1) {
$attrs['step'] = $dateStep;
}
foreach(array('min' => 'dateMin', 'max' => 'dateMax') as $attrName => $propertyName) {
$attrValue = $this->getSetting($propertyName);
if(!$attrValue || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $attrValue)) continue;
$attrs[$attrName] = $attrValue;
}
return "<input " . $this->inputfield->getAttributesString($attrs) . " />";
}
/**
* Render time input
*
* @return string
*
*/
protected function renderTime() {
$timeStep = (int) $this->getSetting('timeStep');
$useSeconds = $timeStep > 0 && $timeStep < 60;
$format = $useSeconds ? InputfieldDatetime::secondsTimeInputFormat : InputfieldDatetime::defaultTimeInputFormat;
$attrs = $this->inputfield->getAttributes();
unset($attrs['size']);
$value = $attrs['value'];
if(!$value && $this->getSetting('defaultToday')) $value = time();
$value = $value ? date($format, $value) : '';
$attrs['type'] = 'time';
$attrs['value'] = $value;
// placeholder and pattern used only if browser does not support HTML5 time
$attrs['placeholder'] = 'hh:mm' . ($useSeconds ? ':ss' : '');
$attrs['pattern'] = '[0-9]{2}:[0-9]{2}' . ($useSeconds ? ':[0-9]{2}' : '');
if($timeStep > 0) {
$attrs['step'] = $timeStep;
}
foreach(array('min' => 'timeMin', 'max' => 'timeMax') as $attrName => $propertyName) {
$attrValue = $this->getSetting($propertyName);
if(!$attrValue || !preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $attrValue)) continue;
$attrs[$attrName] = $attrValue;
}
if($this->getSetting('htmlType') == 'datetime') {
$attrs['name'] .= '__time';
$attrs['id'] .= '__time';
}
return "<input " . $this->inputfield->getAttributesString($attrs) . " />";
}
/**
* @param WireInputData $input
* @return string
*
*/
public function processInput(WireInputData $input) {
$name = $this->getAttribute('name');
$value = $input->$name;
switch($this->getSetting('htmlType')) {
case 'datetime':
$dateValue = trim($input->$name);
$timeName = $name . '__time';
$timeValue = trim($input->$timeName);
// if time present but no date, substitute today's date
if(!strlen($dateValue) && strlen($timeValue)) $dateValue = date('Y-m-d');
$value = strlen($dateValue) ? strtotime(trim("$dateValue $timeValue")) : '';
break;
case 'date':
case 'time':
$value = $this->sanitizeValue($value);
break;
default:
$value = $value ? strtotime($value) : '';
}
return $value;
}
public function sanitizeValue($value) {
$htmlType = $this->getSetting('htmlType');
$value = trim($value);
if(!strlen($value)) return '';
if(ctype_digit($value)) return (int) $value;
if($htmlType === 'time' && !strpos($value, '-') && preg_match('/^\d+:/', $value)) {
// hh:mm:ss
$subDate = $this->inputfield->subDate();
$value = strtotime("$subDate $value");
if($value === false) $value = '';
} else if($htmlType === 'date') {
$subTime = $this->inputfield->subTime();
$value = strtotime("$value $subTime");
} else {
$value = parent::sanitizeValue($value);
}
return $value;
}
/**
* @param InputfieldWrapper $inputfields
*
*/
public function getConfigInputfields(InputfieldWrapper $inputfields) {
/** @var Modules $modules */
$modules = $this->wire('modules');
/** @var InputfieldRadios $f */
$f = $modules->get('InputfieldRadios');
$f->attr('name', 'htmlType');
$f->label = $this->_('HTML input type');
$f->addOption('date', $this->_('Date'));
$f->addOption('time', $this->_('Time'));
$f->addOption('datetime', $this->_('Both date and time'));
$f->val($this->getSetting('htmlType'));
$inputfields->add($f);
/** @var InputfieldInteger $f */
/*
$f = $modules->get('InputfieldInteger');
$f->attr('name', 'dateStep');
$f->label = $this->_('Step days for date input');
if((int) $this->getSetting('dateStep') > 0) $f->attr('value', (int) $this->getSetting('dateStep'));
$f->columnWidth = 33;
$f->showIf = 'htmlType=date|datetime';
$inputfields->add($f);
*/
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('type', 'date');
$f->attr('name', 'dateMin');
$f->label = $this->_('Minimum allowed date');
if($this->getSetting('dateMin')) $f->val($this->getSetting('dateMin'));
$f->showIf = 'htmlType=date|datetime';
$f->columnWidth = 50;
$inputfields->add($f);
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('type', 'date');
$f->inputType = 'html';
$f->attr('name', 'dateMax');
$f->label = $this->_('Maximum allowed date');
if($this->getSetting('dateMax')) $f->val($this->getSetting('dateMax'));
$f->showIf = 'htmlType=date|datetime';
$f->columnWidth = 50;
$inputfields->add($f);
/** @var InputfieldInteger $f */
$f = $modules->get('InputfieldInteger');
$f->attr('name', 'timeStep');
$f->label = $this->_('Step seconds for time input');
if((int) $this->getSetting('timeStep') > 0) $f->attr('value', (int) $this->getSetting('timeStep'));
$f->showIf = 'htmlType=time|datetime';
$f->columnWidth = 33;
$inputfields->add($f);
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('type', 'time');
$f->attr('name', 'timeMin');
$f->label = $this->_('Minimum allowed time');
if($this->getSetting('timeMin')) $f->val($this->getSetting('timeMin'));
$f->showIf = 'htmlType=time|datetime';
$f->columnWidth = 33;
$inputfields->add($f);
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('type', 'time');
$f->attr('name', 'timeMax');
$f->label = $this->_('Maximum allowed time');
if($this->getSetting('timeMax')) $f->val($this->getSetting('timeMax'));
$f->showIf = 'htmlType=time|datetime';
$f->columnWidth = 34;
$inputfields->add($f);
}
}

View File

@@ -0,0 +1,298 @@
<?php namespace ProcessWire;
/**
*
*/
class InputfieldDatetimeSelect extends InputfieldDatetimeType {
public function getDefaultSettings() {
$year = (int) date('Y');
return array(
'dateSelectFormat' => 'yMd',
'timeSelectFormat' => '',
'yearFrom' => $year - 100,
'yearTo' => $year + 20,
'yearLock' => false,
);
}
public function getTypeLabel() {
return $this->_('Separate select inputs for month, day, and year (and optionally time)');
}
/**
* Get years range
*
* @param int $valueYear
* @return array of [ $yearFrom, $yearTo ]
*
*/
protected function getYearsRange($valueYear) {
$defaults = $this->getDefaultSettings();
$yearFrom = $this->getSetting('yearFrom');
$yearTo = $this->getSetting('yearTo');
$yearLock = (int) $this->getSetting('yearLock');
if(!$yearFrom) $yearFrom = $defaults['yearFrom'];
if(!$yearTo) $yearTo = $defaults['yearTo'];
if($yearFrom > $yearTo) {
list($yearFrom, $yearTo) = array($yearTo, $yearFrom);
}
if($valueYear && !$yearLock) {
// there is already a year value present
$numYears = $yearTo > $yearFrom ? ceil(($yearTo - $yearFrom) / 2) : 1;
if($valueYear > $yearTo || $valueYear < $yearFrom) {
// year is before or after that accounted for in selectable range, so change the range
$yearTo = $valueYear + $numYears;
$yearFrom = $valueYear - $numYears;
}
}
return array($yearFrom, $yearTo);
}
/**
* Render ready
*
*/
public function renderReady() {
// "Multi" indicates multiple inputs constructing date/time
$this->inputfield->addClass('InputfieldDatetimeSelect InputfieldDatetimeMulti', 'wrapClass');
}
/**
* @return string
*
*/
public function render() {
$name = $this->getAttribute('name');
$value = $this->getAttribute('value');
$valueYear = $value ? date('Y', $value) : 0;
$yearLock = $this->getSetting('yearLock');
$format = $this->getSetting('dateSelectFormat');
$select = $this->modules->get('InputfieldSelect'); /** @var InputfieldSelect $select */
$sanitizer = $this->wire('sanitizer'); /** @var Sanitizer $sanitizer */
$monthLabel = $this->_('Month');
$yearLabel = $this->_('Year');
$dayLabel = $this->_('Day');
$select->addClass('InputfieldSetWidth');
$months = clone $select;
$months->attr('id+name', $name . '__m');
$months->attr('title', $monthLabel);
$months->addClass('InputfieldDatetimeMonth');
$months->addOption('', $monthLabel);
$abbreviate = strpos($format, 'M') === false;
for($n = 1; $n <= 12; $n++) {
$monthFormat = $abbreviate ? '%b' : '%B';
$monthLabel = $sanitizer->entities(strftime($monthFormat, mktime(0, 0, 0, $n)));
$months->addOption($n, $monthLabel);
}
list($yearFrom, $yearTo) = $this->getYearsRange($valueYear);
$years = clone $select;
$years->attr('id+name', $name . '__y');
$years->attr('title', $yearLabel);
$years->attr('data-from-year', $yearFrom);
$years->attr('data-to-year', $yearTo);
$years->addClass('InputfieldDatetimeYear');
$years->addOption('', $yearLabel);
if(!$yearLock) $years->addOption("-", "< $yearFrom");
for($n = $yearFrom; $n <= $yearTo; $n++) {
$years->addOption($n, $n);
}
if(!$yearLock) $years->addOption("+", "> $yearTo");
$days = clone $select;
$days->attr('id+name', $name . '__d');
$days->attr('title', $dayLabel);
$days->addClass('InputfieldDatetimeDay');
$days->addOption('', $dayLabel);
for($n = 1; $n <= 31; $n++) {
$days->addOption($n, $n);
}
if($value) {
$months->val(date('n', $value));
$days->val(date('j', $value));
$years->val($valueYear);
}
$a = array();
for($n = 0; $n < strlen($format); $n++) {
switch(strtolower($format[$n])) {
case 'm': $a[] = $months->render(); break;
case 'y': $a[] = $years->render(); break;
case 'd': $a[] = $days->render(); break;
}
}
$attrs = $this->inputfield->getAttributes();
$attrs['type'] = 'hidden';
$attrs['value'] = date(InputfieldDatetime::defaultDateInputFormat . ' ' . InputfieldDatetime::defaultTimeInputFormat, $value);
unset($attrs['size'], $attrs['placeholder'], $attrs['class'], $attrs['required']);
$attrs['class'] = 'InputfieldDatetimeValue';
$attrStr = $this->inputfield->getAttributesString($attrs);
$out = implode('&nbsp;', $a) . "<input $attrStr />"; // hidden input for dependencies if needed
return $out;
}
/**
* @param WireInputData $input
* @return string
*
*/
public function processInput(WireInputData $input) {
$name = $this->getAttribute('name');
$a = array(
'second' => 0,
'hour' => 0,
'minute' => 0,
'month' => (int) $input[$name . '__m'],
'year' => (int) $input[$name . '__y'],
'day' => (int) $input[$name . '__d'],
);
if(!strlen(trim("$a[month]$a[day]$a[year]"))) {
// empty value
$this->setAttribute('value', '');
return '';
}
if(empty($a['month'])) $a['month'] = 1;
if($a['month'] > 12) $a['month'] = 12;
if(empty($a['year'])) $a['year'] = date('Y');
if(empty($a['day'])) $a['day'] = 1;
if($a['day'] > 31) $a['day'] = 31;
if((int) $this->getSetting('yearLock')) {
list($yearFrom, $yearTo) = $this->getYearsRange($a['year']);
if($a['year'] < $yearFrom || $a['year'] > $yearTo) {
// year is outside selectable range
$this->setAttribute('value', '');
return '';
}
}
$value = mktime($a['hour'], $a['minute'], $a['second'], $a['month'], $a['day'], $a['year']);
foreach($a as $k => $v) {
if($k === 'year') continue;
if(strlen("$v") === 1) $a[$k] = "0$v";
}
$test1 = "$a[year]-$a[month]-$a[day]"; // $a[hour]:$a[minute]";
$test2 = date('Y-m-d', $value);
if($test1 !== $test2) {
$this->inputfield->error(sprintf($this->_('Invalid date “%1$s” changed to “%2$s”'), $test1, $test2));
}
if($value) $this->setAttribute('value', $value);
return $value;
}
/**
* @param InputfieldWrapper $inputfields
*
*/
public function getConfigInputfields(InputfieldWrapper $inputfields) {
list($y, $d, $h, $hh, $i, $a) = explode(' ', date('Y d h H i A'));
list($m, $mm) = explode(' ', strftime('%b %B'));
$none = $this->_('None');
if($m === $mm && $m === 'May') list($m, $mm) = array('Apr', 'April');
$dateOptions = array(
'' => $none,
'mdy' => "$m $d $y",
'Mdy' => "$mm $d $y",
'dmy' => "$d $m $y",
'dMy' => "$d $mm $y",
'ymd' => "$y $m $d",
'yMd' => "$y $mm $d",
// @todo: add options for 2-part dates (month year, day month, etc.)
//'md_' => "$m $d",
//'dm_' => "$d $m",
//'my.' => "$m $y",
//'ym.' => "$y $m",
//'Md_' => "$mm $d",
//'dM_' => "$d $mm",
//'My.' => "$mm $y",
//'yM.' => "$y $mm"
);
if($mm === $m) {
$abbrLabel = $this->_('(abbreviated)');
foreach($dateOptions as $key => $value) {
if(stripos($key, 'm') !== false) {
$dateOptions[$key] .= " $abbrLabel";
}
}
}
/** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', 'dateSelectFormat');
$f->label = $this->_('Date select format to use');
$f->addOptions($dateOptions);
$f->val($this->getSetting('dateSelectFormat'));
$f->notes = $this->_('Month names are language/locale based');
$inputfields->add($f);
// @todo add time select option
//$f->columnWidth = 50;
$timeOptions = array(
'' => $none,
'hia' => "$h:$i $a",
'Hi' => "$hh:$i"
);
/*
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', 'timeSelectFormat');
$f->label = $this->_('Time select format to use');
$f->addOptions($timeOptions);
$f->val($this->timeSelectFormat);
$f->columnWidth = 50;
$inputfields->add($f);
*/
/** @var InputfieldInteger $f */
$f = $this->modules->get('InputfieldInteger');
$f->attr('name', 'yearFrom');
$f->label = $this->_('First selectable year');
$f->val($this->getSetting('yearFrom'));
$f->columnWidth = 33;
$inputfields->add($f);
/** @var InputfieldInteger $f */
$f = $this->modules->get('InputfieldInteger');
$f->attr('name', 'yearTo');
$f->label = $this->_('Last selectable year');
$f->val($this->getSetting('yearTo'));
$f->columnWidth = 33;
$inputfields->add($f);
/** @var InputfieldToggle $f */
$f = $this->modules->get('InputfieldToggle');
$f->attr('name', 'yearLock');
$f->label = $this->_('Limit selection to these years?');
$f->val((int) $this->getSetting('yearLock'));
$f->columnWidth = 34;
$inputfields->add($f);
}
}

View File

@@ -0,0 +1,410 @@
<?php namespace ProcessWire;
/**
* Text date input types with optional jQuery UI datepicker
*
*/
class InputfieldDatetimeText extends InputfieldDatetimeType {
/**
* jQuery UI datepicker: None
*
*/
const datepickerNo = 0;
/**
* jQuery UI datepicker: Click button to show
*
*/
const datepickerClick = 1;
/**
* jQuery UI datepicker: Inline datepicker always visible (no timepicker support)
*
*/
const datepickerInline = 2;
/**
* jQuery UI datepicker: Show when input focused (recommend option when using datepicker)
*
*/
const datepickerFocus = 3;
/**
* @return array
*
*/
public function getDefaultSettings() {
$a = array(
'datepicker' => self::datepickerNo,
'dateInputFormat' => InputfieldDatetime::defaultDateInputFormat,
'timeInputFormat' => '',
'timeInputSelect' => 0,
'yearRange' => '',
);
if($this->languages) {
foreach($this->languages as $language) {
/** @var Language $language */
// account for alternate formats in other languages
if($language->isDefault()) continue;
$a["dateInputFormat$language"] = '';
$a["timeInputFormat$language"] = '';
}
}
return $a;
}
public function getTypeLabel() {
return $this->_('Text input with jQuery UI datepicker');
}
/**
* Render ready
*
*/
public function renderReady() {
/** @var Config $config */
$config = $this->wire('config');
// this method only needs to run if datepicker is in use
$datepicker = (int) $this->getSetting('datepicker');
if(!$datepicker) return;
list($dateFormat, $timeFormat) = $this->getInputFormat(true);
if($dateFormat) {} // not used here
$useTime = false;
$language = $this->wire('languages') ? $this->wire('user')->language : null;
$this->wire('modules')->get('JqueryCore'); // Jquery Core required before Jquery UI
$this->wire('modules')->get('JqueryUI');
$this->inputfield->addClass("InputfieldDatetimeDatepicker InputfieldDatetimeDatepicker{$datepicker}");
if(strlen($timeFormat) && $datepicker != self::datepickerInline) {
// add in the timepicker script, if applicable
$useTime = true;
$url = $config->urls->get('InputfieldDatetime');
$config->scripts->add($url . 'timepicker/jquery-ui-timepicker-addon.min.js');
$config->styles->add($url . 'timepicker/jquery-ui-timepicker-addon.min.css');
}
if($language) {
// include i18n support for the datepicker
// note that the 'xx' in the filename is just a placeholder to indicate what should be replaced for translations, as that file doesn't exist
$langFile = ltrim($this->_('/wire/modules/Jquery/JqueryUI/i18n/jquery.ui.datepicker-xx.js'), '/'); // Datepicker translation file // Replace 'xx' with jQuery UI language code or specify your own js file
if(is_file($config->paths->root . $langFile)) {
// add a custom language file
$config->scripts->add($config->urls->root . $langFile);
} else {
// attempt to auto-find one based on the language name (which are often 2 char language codes)
$langFile = "wire/modules/Jquery/JqueryUI/i18n/jquery.ui.datepicker-{$language->name}.js";
if(is_file($config->paths->root . $langFile)) $config->scripts->add($config->urls->root . $langFile);
}
if($useTime) {
$langFile = $this->_('timepicker/i18n/jquery-ui-timepicker-xx.js'); // Timepicker translation file // Replace 'xx' with jQuery UI language code or specify your own js file. Timepicker i18n files are located in /wire/modules/Inputfield/InputfieldDatetime/timepicker/i18n/.
$path = $config->paths->get('InputfieldDatetime');
$url = $config->urls->get('InputfieldDatetime');
if(is_file($path . $langFile)) {
// add a custom language file
$config->scripts->add($url . $langFile);
} else {
// attempt to auto-find one based on the language name (which are often 2 char language codes)
$langFile = str_replace('-xx.', "-$language->name.", $langFile);
if(is_file($path . $langFile)) {
$config->scripts->add($url . $langFile);
}
}
}
}
}
/**
* @return string
*
*/
public function render() {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
/** @var WireDateTime $datetime */
$datetime = $this->wire('datetime');
$datepicker = (int) $this->getSetting('datepicker');
list($dateFormat, $timeFormat) = $this->getInputFormat(true);
$useTime = false;
if(strlen($timeFormat) && $datepicker && $datepicker != self::datepickerInline) $useTime = true;
$attrs = $this->inputfield->getAttributes();
$value = $attrs['value'];
$valueTS = (int) $value*1000; // TS=for datepicker/javascript, which uses milliseconds rather than seconds
unset($attrs['value']);
if(!$value && $this->inputfield->getSetting('defaultToday')) {
$value = date($dateFormat);
if($timeFormat) $value .= ' ' . date($timeFormat);
$valueTS = time()*1000;
} else if($value) {
$value = trim(date($dateFormat . ' ' . $timeFormat, (int) $value));
}
$value = $sanitizer->entities($value);
$dateFormatJS = $sanitizer->entities($datetime->convertDateFormat($dateFormat, 'js'));
$timeFormatJS = $useTime ? $datetime->convertDateFormat($timeFormat, 'js') : '';
if(strpos($timeFormatJS, 'h24') !== false) {
// 24 hour format
$timeFormatJS = str_replace(array('hh24', 'h24'), array('HH', 'H'), $timeFormatJS);
$ampm = 0;
} else {
$ampm = 1;
}
if(strlen($timeFormatJS)) $timeFormatJS = $sanitizer->entities($timeFormatJS);
if(empty($value)) $value = '';
$yearRange = $sanitizer->entities($this->getSetting('yearRange'));
$timeInputSelect = $this->getSetting('timeInputSelect');
$out =
"<input " . $this->inputfield->getAttributesString($attrs) . " " .
"value='$value' " .
"data-dateformat='$dateFormatJS' " .
"data-timeformat='$timeFormatJS' " .
"data-timeselect='$timeInputSelect' " .
"data-ts='$valueTS' " .
"data-ampm='$ampm' " .
(strlen($yearRange) ? "data-yearrange='$yearRange' " : '') .
"/>";
return $out;
}
/**
* Render value
*
* @return string
*
*/
public function renderValue() {
$value = $this->getAttribute('value');
$format = $this->getSetting('dateInputFormat') . ' ' . $this->getSetting('timeInputFormat');
return $format && $value ? $this->wire('datetime')->formatDate($value, trim($format)) : '';
}
/**
* @param WireInputData $input
* @return int|string|bool
*
*/
public function processInput(WireInputData $input) {
return false; // tell InputfieldDatetime to process the input instead
}
/**
* Get the input format string for the user's language
*
* @param bool $getArray
* @return string|array of dateInputFormat timeInputFormat
*
*/
protected function getInputFormat($getArray = false) {
$inputFormats = array();
$language = $this->wire('user')->language;
$useLanguages = $this->wire('languages') && $language && !$language->isDefault();
foreach(array('date', 'time') as $type) {
$inputFormat = '';
if($useLanguages) {
$inputFormat = trim($this->getSetting("{$type}InputFormat{$language->id}"));
}
if(!strlen($inputFormat)) {
// fallback to default language
$inputFormat = $this->getSetting("{$type}InputFormat");
}
$inputFormats[] = $inputFormat;
}
if($getArray) return $inputFormats;
return trim(implode(' ', $inputFormats));
}
/**
* Sanitize value
*
* @param int|string $value
* @return int|string
*
*/
public function sanitizeValue($value) {
// convert date string to unix timestamp
$format = $this->getInputFormat();
$value = $this->wire('datetime')->stringToTimestamp($value, $format);
return $value;
}
/**
* @param InputfieldWrapper $inputfields
*
*/
public function getConfigInputfields(InputfieldWrapper $inputfields) {
$languages = $this->wire('languages');
$datetime = $this->wire('datetime');
$dateInputFormat = $this->getSetting('dateInputFormat');
$timeInputFormat = $this->getSetting('timeInputFormat');
$timeInputSelect = (int) $this->getSetting('timeInputSelect');
/** @var InputfieldRadios $f */
$f = $this->modules->get('InputfieldRadios');
$f->label = $this->_('Date Picker');
$f->setAttribute('name', 'datepicker');
$f->addOption(self::datepickerNo, $this->_('No date/time picker'));
$f->addOption(self::datepickerFocus, $this->_('Date/time picker on field focus') . ' ' .
$this->_('(recommended)'));
$f->addOption(self::datepickerClick, $this->_('Date/time picker on button click'));
// @todo this datepickerInline option displays a datepicker that is too large, not fully styled
$f->addOption(self::datepickerInline, $this->_('Inline date picker always visible (no time picker)'));
$f->attr('value', (int) $this->getSetting('datepicker'));
$inputfields->append($f);
/** @var InputfieldFieldset $fieldset */
$fieldset = $this->modules->get('InputfieldFieldset');
$fieldset->attr('name', '_dateTimeInputFormats');
$fieldset->label = $this->_('Date/Time Input Formats');
/** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', '_dateInputFormat');
$f->label = $this->_('Date Input Format');
$f->description = $this->_('Select the format to be used for user input to this field. Your selection will populate the field below this, which you may customize further if needed.');
$f->icon = 'calendar';
$date = strtotime('2016-04-08 5:10:02 PM');
foreach($datetime->getDateFormats() as $format) {
$dateFormatted = $datetime->formatDate($date, $format);
if($format == 'U') $dateFormatted .= " " . $this->_('(unix timestamp)');
$f->addOption($format, $dateFormatted);
if($dateInputFormat == $format) $f->attr('value', $format);
}
$f->attr('onchange', "$('#Inputfield_dateInputFormat').val($(this).val());");
$fieldset->add($f);
/** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', '_timeInputFormat');
$f->label = $this->_('Time Input Format');
$f->addOption('', $this->_('None'));
$f->description = $this->_('Select an optional time format to be used for input. If used, the calendar option will include a time picker.');
$f->icon = 'clock-o';
foreach($datetime->getTimeFormats() as $format) {
if(strpos($format, '!') === 0) continue; // skip relative formats
$timeFormatted = $datetime->formatDate($date, $format);
$f->addOption($format, $timeFormatted);
if($timeInputFormat == $format) $f->attr('value', $format);
}
$f->attr('onchange', "$('#Inputfield_timeInputFormat').val($(this).val());");
$f->collapsed = Inputfield::collapsedBlank;
$f->columnWidth = 50;
$fieldset->add($f);
/** @var InputfieldRadios $f */
$f = $this->modules->get("InputfieldRadios");
$f->attr('name', 'timeInputSelect');
$f->label = $this->_('Time Input Type');
$f->description = $this->_('Sliders (default) let the user slide controls to choose the time, where as Select lets the user select the time from a drop-down select.');
$f->icon = 'clock-o';
$f->addOption(0, $this->_('Sliders'));
$f->addOption(1, $this->_('Select'));
$f->optionColumns = 1;
$f->columnWidth = 50;
$f->showIf = "_timeInputFormat!='', datepicker!=" . self::datepickerNo;
$f->attr('value', $timeInputSelect);
$fieldset->add($f);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'dateInputFormat');
$f->attr('value', $dateInputFormat ? $dateInputFormat : InputfieldDatetime::defaultDateInputFormat);
$f->attr('size', 20);
$f->label = $this->_('Date Input Format Code');
$f->description = $this->_('This is automatically built from the date select above, unless you modify it.');
$f->icon = 'calendar';
$notes = $this->_('See the [PHP date](http://www.php.net/manual/en/function.date.php) function reference for more information on how to customize these formats.');
if($languages) $notes .= "\n" . $this->_('You may optionally specify formats for other languages here as well. Any languages left blank will inherit the default setting.');
$f->notes = $notes;
$f->collapsed = Inputfield::collapsedYes;
$f1 = $f;
$fieldset->add($f);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'timeInputFormat');
$f->attr('value', $timeInputFormat ? $timeInputFormat : '');
$f->attr('size', 20);
$f->label = $this->_('Time Input Format Code');
$f->description = $this->_('This is automatically built from the time select above, unless you modify it.');
$f->icon = 'clock-o';
$f->notes = $notes;
$f->collapsed = Inputfield::collapsedYes;
$f2 = $f;
if($languages) {
$f1->useLanguages = true;
$f2->useLanguages = true;
foreach($languages as $language) {
if($language->isDefault()) continue;
$f1->set("value$language", (string) $this->getSetting("dateInputFormat$language"));
$f2->set("value$language", (string) $this->getSetting("timeInputFormat$language"));
}
}
$fieldset->add($f1);
$fieldset->add($f2);
$inputfields->add($fieldset);
/** @var InputfieldText $field */
$f = $this->modules->get('InputfieldText');
$f->setAttribute('name', 'placeholder');
$f->label = $this->_('Placeholder Text');
$f->setAttribute('value', $this->getAttribute('placeholder'));
$f->description = $this->_('Optional placeholder text that appears in the field when blank.');
$f->columnWidth = 50;
$inputfields->append($f);
/** @var InputfieldInteger $f */
$f = $this->modules->get('InputfieldInteger');
$f->setAttribute('name', 'size');
$f->label = $this->_('Size');
$f->attr('value', $this->getAttribute('size'));
$f->attr('size', 4);
$f->description = $this->_('The displayed width of this field (in characters).');
$f->columnWidth = 50;
$inputfields->append($f);
/** @var InputfieldText $f */
$f = $this->modules->get("InputfieldText");
$f->attr('name', 'yearRange');
$f->attr('value', $this->getSetting('yearRange'));
$f->attr('size', 10);
$f->label = $this->_('Year Range');
$f->description =
$this->_('When predefined year selection is possible, there is a range of plus or minus 10 years from the current year.') . ' ' .
$this->_('To modify this range, specify number of years before and after current year in this format: `-30:+20`, which would show 30 years before now and 20 years after now.');
$f->notes = $this->_('Default is `-10:+10` which shows a year range 10 years before and after now.');
$f->icon = 'arrows-h';
$f->collapsed = Inputfield::collapsedBlank;
$inputfields->append($f);
}
}