* @since 1.0
*/
class DateRangePicker extends InputWidget
{
/**
* @var string the javascript callback to be passed to the plugin constructor. Note: a default value is set for
* this property when you set [[hideInput]] to false, OR you set [[useWithAddon]] to `true` or [[autoUpdateOnInit]]
* to `false`. If you set a value here it will override any auto-generated callbacks.
*/
public $callback = null;
/**
* @var boolean whether to auto update the input on initialization. If set to `false`, this will auto set the
* `pluginOptions['autoUpdateInput']` to `false`. A default [[callback]] will be auto-generated when this is set to
* `false`.
*/
public $autoUpdateOnInit = false;
/**
* @var boolean whether to hide the input (e.g. when you want to show the date range picker as a dropdown). If set
* to `true`, the input will be hidden. The plugin will be initialized on a container element (default 'div'),
* using the container template. A default `callback` will be setup in this case to display the selected range
* value within the container.
*/
public $hideInput = false;
/**
* @var boolean whether you are using the picker with a input group addon. You can set it to `true`, when
* `hideInput` is false, and you wish to show the picker position more correctly at the input-group-addon icon.
* A default `callback` will be generated in this case to generate the selected range value for the input.
*/
public $useWithAddon = false;
/**
* @var boolean initialize all the list values set in `pluginOptions['ranges']` and convert all values to
* `yii\web\JsExpression`
*/
public $initRangeExpr = true;
/**
* @var boolean show a preset dropdown. If set to true, this will automatically generate a preset list of ranges
* for selection. Setting this to true will also automatically set `initRangeExpr` to true.
*/
public $presetDropdown = false;
/**
* @var array the HTML attributes for the container, if hideInput is set to true. The following special options
* are recognized:
* - `tag`: string, the HTML tag for rendering the container. Defaults to `div`.
*/
public $containerOptions = [];
/**
* @var string the attribute name which you can set optionally to track changes to the range start value. One of
* the following actions will be taken when this is set:
* - If using with model, an active hidden input will be automatically generated using this as an attribute name
* for the start value of the range.
* - If using without model, a normal hidden input will be automatically generated using this as an input name
* for the start value of the range.
*/
public $startAttribute;
/**
* @var string the attribute name which you can set optionally to track changes to the range end value. One of
* the following actions will be taken when this is set:
* - If using with model, an active hidden input will be automatically generated using this as an attribute name
* for the end value of the range.
* - If using without model, a normal hidden input will be automatically generated using this as an input name
* for the end value of the range.
*/
public $endAttribute;
/**
* @var array the HTML attributes for the start input (applicable only if `startAttribute` is set). If using
* without a model, you can set a start value here within the `value` property.
*/
public $startInputOptions = [];
/**
* @var array the HTML attributes for the end input (applicable only if `endAttribute` is set). If using
* without a model, you can set an end value here within the `value` property.
*/
public $endInputOptions = [];
/**
* @var array the template for rendering the container, when hideInput is set to `true`. The special tag `{input}`
* will be replaced with the hidden form input. In addition, the element with css class `range-value` will be
* replaced by the calculated plugin value. The special tag `{value}` will be replaced with the value of the hidden
* form input during initialization
*/
public $containerTemplate = <<< HTML
{value}
{input}
HTML;
/**
* @var boolean whether to HTML encode the value
*/
public $encodeValue = true;
/**
* HTML attributes for the `span` element that displays the default value for a preset dropdown. By default for a
* preset dropdown, if the value is empty, it will default to "Today". The following special options are supported:
* - `tag`: the HTML tag under which the default value markup will be displayed when empty. Defaults to `em`.
*/
public $defaultPresetValueOptions = ['class' => 'text-muted'];
/**
* @var array the HTML attributes for the form input
*/
public $options = ['class' => 'form-control'];
/**
* @inheritdoc
*/
public $pluginName = 'daterangepicker';
/**
* @var string locale language to be used for the plugin
*/
protected $_localeLang = '';
/**
* @var string the pluginOptions format for the date time
*/
protected $_format;
/**
* @var string the pluginOptions separator
*/
protected $_separator;
/**
* @var string the generated input for start attribute when `startAttribute` has been set
*/
protected $_startInput = '';
/**
* @var string the generated input for end attribute when `endAttribute` has been set
*/
protected $_endInput = '';
/**
* Automatically convert the date format from PHP DateTime to Moment.js DateTime format as required by the
* `bootstrap-daterangepicker` plugin.
*
* @see http://php.net/manual/en/function.date.php
* @see http://momentjs.com/docs/#/parsing/string-format/
*
* @param string $format the PHP date format string
*
* @return string
*/
protected static function convertDateFormat($format)
{
$conversions = [
// meridian lowercase remains same
// 'a' => 'a',
// meridian uppercase remains same
// 'A' => 'A',
// second (with leading zeros)
's' => 'ss',
// minute (with leading zeros)
'i' => 'mm',
// hour in 12-hour format (no leading zeros)
'g' => 'h',
// hour in 12-hour format (with leading zeros)
'h' => 'hh',
// hour in 24-hour format (no leading zeros)
'G' => 'H',
// hour in 24-hour format (with leading zeros)
'H' => 'HH',
// day of the week locale
'w' => 'e',
// day of the week ISO
'W' => 'E',
// day of month (no leading zero)
'j' => 'D',
// day of month (two digit)
'd' => 'DD',
// day name short
'D' => 'DDD',
// day name long
'l' => 'DDDD',
// month of year (no leading zero)
'n' => 'M',
// month of year (two digit)
'm' => 'MM',
// month name short
'M' => 'MMM',
// month name long
'F' => 'MMMM',
// year (two digit)
'y' => 'YY',
// year (four digit)
'Y' => 'YYYY',
// unix timestamp
'U' => 'X',
];
return strtr($format, $conversions);
}
/**
* Parses and returns a JsExpression
*
* @param string|JsExpression $value
*
* @return JsExpression
*/
protected static function parseJsExpr($value)
{
return $value instanceof JsExpression ? $value : new JsExpression($value);
}
/**
* @inheritdoc
*/
public function run()
{
$this->initSettings();
echo $this->renderInput();
}
/**
* Registers the needed client assets
*/
public function registerAssets()
{
$view = $this->getView();
MomentAsset::register($view);
$input = 'jQuery("#' . $this->options['id'] . '")';
$id = $input;
if ($this->hideInput) {
$id = 'jQuery("#' . $this->containerOptions['id'] . '")';
}
if (!empty($this->_langFile)) {
LanguageAsset::register($view)->js[] = $this->_langFile;
}
DateRangePickerAsset::register($view);
$rangeJs = '';
if (empty($this->callback)) {
$val = "start.format('{$this->_format}') + '{$this->_separator}' + end.format('{$this->_format}')";
if (ArrayHelper::getValue($this->pluginOptions, 'singleDatePicker', false)) {
$val = "start.format('{$this->_format}')";
}
$rangeJs = $this->getRangeJs('start') . $this->getRangeJs('end');
$change = $rangeJs . "{$input}.val(val).trigger('change');";
if ($this->presetDropdown) {
$id = "{$id}.find('.kv-drp-dropdown')";
}
if ($this->hideInput) {
$script = "var val={$val};{$id}.find('.range-value').html(val);{$change}";
} elseif ($this->useWithAddon) {
$id = "{$input}.closest('.input-group')";
$script = "var val={$val};{$change}";
} elseif (!$this->autoUpdateOnInit) {
$script = "var val={$val};{$change}";
} else {
$this->registerPlugin($this->pluginName, $id);
return;
}
$this->callback = "function(start,end,label){{$script}}";
}
$nowFrom = "moment().startOf('day').format('{$this->_format}')";
$nowTo = "moment().format('{$this->_format}')";
// parse input change correctly when range input value is cleared
$js = <<< JS
{$input}.off('change.kvdrp').on('change.kvdrp', function() {
var drp = {$id}.data('{$this->pluginName}'), fm, to;
if ($(this).val() || !drp) {
return;
}
fm = {$nowFrom} || '';
to = {$nowTo} || '';
drp.setStartDate(fm);
drp.setEndDate(to);
{$rangeJs}
});
JS;
if ($this->presetDropdown && empty($this->value)) {
$tag = ArrayHelper::remove($this->defaultPresetValueOptions, 'tag', 'em');
$fmTag = Html::beginTag($tag, $this->defaultPresetValueOptions);
$toTag = Html::endTag($tag);
$js .= "var val={$nowFrom}+'{$this->_separator}'+{$nowTo};{$id}.find('.range-value').html('{$fmTag}'+val+'{$toTag}');";
}
$view->registerJs($js);
$this->registerPlugin($this->pluginName, $id, null, $this->callback);
}
/**
* Initializes widget settings
*
* @throws InvalidConfigException
*/
protected function initSettings()
{
$this->_msgCat = 'kvdrp';
$this->initI18N(__DIR__);
$this->initLocale();
if ($this->convertFormat && isset($this->pluginOptions['locale']['format'])) {
$this->pluginOptions['locale']['format'] = static::convertDateFormat(
$this->pluginOptions['locale']['format']
);
}
$locale = ArrayHelper::getValue($this->pluginOptions, 'locale', []);
$this->_format = ArrayHelper::getValue($locale, 'format', 'YYYY-MM-DD');
$this->_separator = ArrayHelper::getValue($locale, 'separator', ' - ');
if (!empty($this->value)) {
if ($this->encodeValue) {
$this->value = Html::encode($this->value);
}
$dates = explode($this->_separator, $this->value);
if (count($dates) > 1) {
$this->pluginOptions['startDate'] = $dates[0];
$this->pluginOptions['endDate'] = $dates[1];
$this->initRangeValue('start', $dates[0]);
$this->initRangeValue('end', $dates[1]);
}
if ($this->startAttribute && $this->endAttribute) {
$start = $this->getRangeValue('start');
$end = $this->getRangeValue('end');
$this->value = $start . $this->_separator . $end;
if ($this->hasModel()) {
$attr = $this->attribute;
$this->model->$attr = $this->value;
}
$this->pluginOptions['startDate'] = $start;
$this->pluginOptions['endDate'] = $end;
}
}
$value = empty($this->value) ? '' : $this->value;
$this->containerTemplate = str_replace('{value}', $value, $this->containerTemplate);
// Set `autoUpdateInput` to false for certain settings
if (!$this->autoUpdateOnInit || $this->hideInput || $this->useWithAddon) {
$this->pluginOptions['autoUpdateInput'] = false;
}
$this->_startInput = $this->getRangeInput('start');
$this->_endInput = $this->getRangeInput('end');
if (empty($this->containerOptions['id'])) {
$this->containerOptions['id'] = $this->options['id'] . '-container';
}
if (empty($this->containerOptions['class'])) {
$css = $this->useWithAddon && !$this->presetDropdown && !$this->hideInput ? ' input-group' : '';
$this->containerOptions['class'] = 'kv-drp-container' . $css;
}
$this->initRange();
$this->registerAssets();
}
/**
* Initialize locale settings
*/
protected function initLocale()
{
$this->setLanguage('');
if (empty($this->_langFile)) {
return;
}
$localeSettings = ArrayHelper::getValue($this->pluginOptions, 'locale', []);
$localeSettings += [
'applyLabel' => Yii::t('kvdrp', 'Apply'),
'cancelLabel' => Yii::t('kvdrp', 'Cancel'),
'fromLabel' => Yii::t('kvdrp', 'From'),
'toLabel' => Yii::t('kvdrp', 'To'),
'weekLabel' => Yii::t('kvdrp', 'W'),
'customRangeLabel' => Yii::t('kvdrp', 'Custom Range'),
'daysOfWeek' => new JsExpression('moment.weekdaysMin()'),
'monthNames' => new JsExpression('moment.monthsShort()'),
'firstDay' => new JsExpression('moment.localeData()._week.dow'),
];
$this->pluginOptions['locale'] = $localeSettings;
}
/**
* Initializes the pluginOptions range list
*/
protected function initRange()
{
if (isset($dummyValidation)) {
/** @noinspection PhpUnusedLocalVariableInspection */
$msg = Yii::t('kvdrp', 'Select Date Range');
}
$m = 'moment()';
if ($this->presetDropdown) {
$this->initRangeExpr = $this->hideInput = true;
$this->pluginOptions['opens'] = ArrayHelper::getValue($this->pluginOptions, 'opens', 'left');
$this->pluginOptions['ranges'] = [
Yii::t('kvdrp', 'Today') => ["{$m}.startOf('day')", $m],
Yii::t('kvdrp', 'Yesterday') => [
"{$m}.startOf('day').subtract(1,'days')",
"{$m}.endOf('day').subtract(1,'days')",
],
Yii::t('kvdrp', 'Last {n} Days', ['n' => 7]) => ["{$m}.startOf('day').subtract(6, 'days')", $m],
Yii::t('kvdrp', 'Last {n} Days', ['n' => 30]) => ["{$m}.startOf('day').subtract(29, 'days')", $m],
Yii::t('kvdrp', 'This Month') => ["{$m}.startOf('month')", "{$m}.endOf('month')"],
Yii::t('kvdrp', 'Last Month') => [
"{$m}.subtract(1, 'month').startOf('month')",
"{$m}.subtract(1, 'month').endOf('month')",
],
];
if (empty($this->value)) {
$this->pluginOptions['startDate'] = new JsExpression("{$m}.startOf('day')");
$this->pluginOptions['endDate'] = new JsExpression($m);
}
}
$opts = $this->pluginOptions;
if (!$this->initRangeExpr || empty($opts['ranges']) || !is_array($opts['ranges'])) {
return;
}
$range = [];
foreach ($opts['ranges'] as $key => $value) {
if (!is_array($value) || empty($value[0]) || empty($value[1])) {
throw new InvalidConfigException(
"Invalid settings for pluginOptions['ranges']. Each range value must be a two element array."
);
}
$range[$key] = [static::parseJsExpr($value[0]), static::parseJsExpr($value[1])];
}
$this->pluginOptions['ranges'] = $range;
}
/**
* Renders the input
*
* @return string
*/
protected function renderInput()
{
$append = $this->_startInput . $this->_endInput;
if (!$this->hideInput) {
return $this->getInput('textInput') . $append;
}
$content = str_replace('{input}', $this->getInput('hiddenInput') . $append, $this->containerTemplate);
$tag = ArrayHelper::remove($this->containerOptions, 'tag', 'div');
return Html::tag($tag, $content, $this->containerOptions);
}
/**
* Gets input options based on type
*
* @param string $type whether `start` or `end`
*
* @return array|mixed
*/
protected function getInputOpts($type = '')
{
$opts = $type . 'InputOptions';
return isset($this->$opts) && is_array($this->$opts) ? $this->$opts : [];
}
/**
* Sets input options for a specific type
*
* @param string $type whether `start` or `end`
* @param array $options the options to set
*/
protected function setInputOpts($type = '', $options = [])
{
$opts = $type . 'InputOptions';
if (property_exists($this, $opts)) {
$this->$opts = $options;
}
}
/**
* Gets the range attribute value based on type
*
* @param string $type whether `start` or `end`
*
* @return mixed|string
*/
protected function getRangeAttr($type = '')
{
$attr = $type . 'Attribute';
return $type && isset($this->$attr) ? $this->$attr : '';
}
/**
* Generates and returns the client script on date range change, when the start and end attributes are set
*
* @param string $type whether `start` or `end`
*
* @return string
*/
protected function getRangeJs($type = '')
{
$rangeAttr = $this->getRangeAttr($type);
if (empty($rangeAttr)) {
return '';
}
$options = $this->getInputOpts($type);
$input = "jQuery('#" . $this->options['id'] . "')";
return "var v={$input}.val() ? {$type}.format('{$this->_format}') : '';jQuery('#" . $options['id'] .
"').val(v).trigger('change');";
}
/**
* Generates and returns the hidden input markup when one of start or end attributes are set.
*
* @param string $type whether `start` or `end`
*
* @return string
*/
protected function getRangeInput($type = '')
{
$attr = $this->getRangeAttr($type);
if (empty($attr)) {
return '';
}
$options = $this->getInputOpts($type);
if (empty($options['id'])) {
$options['id'] = $this->options['id'] . '-' . $type;
}
if ($this->hasModel()) {
$this->setInputOpts($type, $options);
return Html::activeHiddenInput($this->model, $attr, $options);
}
$options['type'] = 'hidden';
$options['name'] = $attr;
$this->setInputOpts($type, $options);
return Html::tag('input', '', $options);
}
/**
* Initializes the range values when one of start or end attributes are set.
*
* @param string $type whether `start` or `end`
* @param string $value the value to set
*/
protected function initRangeValue($type = '', $value = '')
{
$attr = $this->getRangeAttr($type);
if (empty($attr) || empty($value)) {
return;
}
if ($this->hasModel()) {
$this->model->$attr = $value;
} else {
$options = $this->getInputOpts($type);
$options['value'] = $value;
$this->setInputOpts($type, $options);
}
}
/**
* Generates and returns the hidden input markup when one of start or end attributes are set.
*
* @param string $type whether `start` or `end`
*
* @return string
*/
protected function getRangeValue($type = '')
{
$attr = $this->getRangeAttr($type);
if (empty($attr)) {
return '';
}
$options = $this->getInputOpts($type);
return $this->hasModel() ? Html::getAttributeValue($this->model, $attr) :
ArrayHelper::getValue($options, 'value', '');
}
}