* @since 1.0
* @see http://eternicode.github.io/bootstrap-datepicker/
*/
class DatePicker extends \kartik\base\InputWidget
{
const CALENDAR_ICON = '';
const TYPE_INPUT = 1;
const TYPE_COMPONENT_PREPEND = 2;
const TYPE_COMPONENT_APPEND = 3;
const TYPE_INLINE = 4;
const TYPE_RANGE = 5;
const TYPE_BUTTON = 6;
/**
* @var string the markup type of widget markup
* must be one of the TYPE constants. Defaults
* to [[TYPE_COMPONENT_PREPEND]]
*/
public $type = self::TYPE_COMPONENT_PREPEND;
/**
* @var string The size of the input - 'lg', 'md', 'sm', 'xs'
*/
public $size;
/**
* @var ActiveForm the ActiveForm object which you can pass for seamless usage
* with ActiveForm. This property is especially useful for client validation of
* attribute2 for [[TYPE_RANGE]] validation
*/
public $form;
/**
* @var array the HTML attributes for the button that is rendered for [[DatePicker::TYPE_BUTTON]].
* Defaults to `['class'=>'btn btn-default']`. The following special options are recognized:
* - 'label': string the button label. Defaults to ``
*/
public $buttonOptions = [];
/**
* @var mixed the calendar picker button configuration.
* - if this is passed as a string, it will be displayed as is (will not be HTML encoded).
* - if this is set to false, the picker button will not be displayed.
* - if this is passed as an array (this is the DEFAULT) it will treat this as HTML attributes
* for the button (to be displayed as a Bootstrap addon). The following special keys are recognized;
* - icon - string, the bootstrap glyphicon name/suffix. Defaults to 'calendar'.
* - title - string|bool, the title to be displayed on hover. Defaults to 'Select date & time'. To disable,
* set it to `false`.
*/
public $pickerButton = [];
/**
* @var mixed the calendar remove button configuration - applicable only for type
* set to `DatePicker::TYPE_COMPONENT_PREPEND` or `DatePicker::TYPE_COMPONENT_APPEND`.
* - if this is passed as a string, it will be displayed as is (will not be HTML encoded).
* - if this is set to false, the remove button will not be displayed.
* - if this is passed as an array (this is the DEFAULT) it will treat this as HTML attributes
* for the button (to be displayed as a Bootstrap addon). The following special keys are recognized;
* - icon - string, the bootstrap glyphicon name/suffix. Defaults to 'remove'.
* - title - string, the title to be displayed on hover. Defaults to 'Clear field'. To disable,
* set it to `false`.
*/
public $removeButton = [];
/**
* @var array the HTML attributes for the input tag.
*/
public $options = [];
/**
* @var array The addon that will be prepended/appended for a [[TYPE_COMPONENT_PREPEND]] and
* [[TYPE_COMPONENT_APPEND]]. You can set the following array keys:
* - part1: string, the content to prepend before the [[TYPE_COMPONENT_PREPEND]] OR
* before input # 1 for [[TYPE_RANGE]].
* - part2: string, the content to prepend after the [[TYPE_COMPONENT_PREPEND]] OR
* after input # 1 for [[TYPE_RANGE]].
* - part3: string, the content to append before the [[TYPE_COMPONENT_APPEND]] OR
* before input # 2 for [[TYPE_RANGE]].
* - part4: string, the content to append after the [[TYPE_COMPONENT_APPEND]] OR
* after input # 2 for [[TYPE_RANGE]].
*/
public $addon = [];
/**
* @var string the model attribute 2 if you are using [[TYPE_RANGE]]
* for markup.
*/
public $attribute2;
/**
* @var string the name of input number 2 if you are using [[TYPE_RANGE]]
* for markup
*/
public $name2;
/**
* @var string the name of value for input number 2 if you are using [[TYPE_RANGE]]
* for markup
*/
public $value2 = null;
/**
* @var array the HTML attributes for the input number 2 tag.
* if you are using [[TYPE_RANGE]] for markup
*/
public $options2 = [];
/**
* @var string the range input separator
* if you are using [[TYPE_RANGE]] for markup.
* Defaults to 'to'
*/
public $separator = 'to';
/**
* @var array the HTML options for the DatePicker container
*/
private $_container = [];
/**
* @var bool whether a prepend or append addon exists
*/
protected $_hasAddon = false;
/**
* Initializes the widget
*
* @throw InvalidConfigException
*/
public function init()
{
$this->_msgCat = 'kvdate';
$this->pluginName = 'kvDatepicker';
parent::init();
$this->_hasAddon = $this->type == self::TYPE_COMPONENT_PREPEND || $this->type == self::TYPE_COMPONENT_APPEND;
if ($this->type === self::TYPE_RANGE && $this->attribute2 === null && $this->name2 === null) {
throw new InvalidConfigException("Either 'name2' or 'attribute2' properties must be specified for a datepicker 'range' markup.");
}
if ($this->type === self::TYPE_RANGE && !class_exists('\\kartik\\field\\FieldRangeAsset')) {
throw new InvalidConfigException("The yii2-field-range extension is not installed and is a pre-requisite for a DatePicker RANGE type. To install this extension run this command on your console: \n\nphp composer.phar require kartik-v/yii2-field-range \"*\"");
}
if ($this->type < 1 || $this->type > 6 || !is_int($this->type)) {
throw new InvalidConfigException("Invalid value for the property 'type'. Must be an integer between 1 and 6.");
}
if (isset($this->form) && !($this->form instanceof \yii\widgets\ActiveForm)) {
throw new InvalidConfigException("The 'form' property must be of type \\yii\\widgets\\ActiveForm");
}
if (isset($this->form) && !$this->hasModel()) {
throw new InvalidConfigException("You must set the 'model' and 'attribute' properties when the 'form' property is set.");
}
if (isset($this->form) && ($this->type === self::TYPE_RANGE) && (!isset($this->attribute2))) {
throw new InvalidConfigException("The 'attribute2' property must be set for a 'range' type markup and a defined 'form' property.");
}
if (isset($this->addon) && !is_array($this->addon)) {
throw new InvalidConfigException("The 'addon' property must be setup as an array with 'part1', 'part2', 'part3', and/or 'part4' keys.");
}
$s = DIRECTORY_SEPARATOR;
$this->initI18N(__DIR__);
$this->setLanguage('bootstrap-datepicker.', __DIR__ . "{$s}assets{$s}", null, '.min.js');
$this->parseDateFormat('date');
$this->_container['id'] = $this->options['id'] . '-' . $this->_msgCat;
if ($this->type == self::TYPE_INLINE) {
$this->_container['data-date'] = $this->value;
}
$this->options['data-datepicker-source'] = $this->type == self::TYPE_INPUT ? $this->options['id'] : $this->_container['id'];
$this->options['data-datepicker-type'] = $this->type;
$this->registerAssets();
echo $this->renderInput();
}
/**
* Renders the source input for the DatePicker plugin.
* Graceful fallback to a normal HTML text input - in
* case JQuery is not supported by the browser
*/
protected function renderInput()
{
if ($this->type == self::TYPE_INLINE) {
if (empty($this->options['readonly'])) {
$this->options['readonly'] = true;
}
if (empty($this->options['class'])) {
$this->options['class'] = 'form-control input-sm text-center';
}
} else {
Html::addCssClass($this->options, 'form-control');
}
if (isset($this->form) && ($this->type !== self::TYPE_RANGE)) {
$vars = call_user_func('get_object_vars', $this);
unset($vars['form']);
return $this->form->field($this->model, $this->attribute)->widget(self::classname(), $vars);
}
$input = $this->type == self::TYPE_BUTTON ? 'hiddenInput' : 'textInput';
return $this->parseMarkup($this->getInput($input));
}
/**
* Returns the addon to render
*
* @param array $options the HTML attributes for the addon
* @param string $type whether the addon is the picker or remove
* @return string
*/
protected function renderAddon(&$options, $type = 'picker')
{
if ($options === false) {
return '';
}
if (is_string($options)) {
return $options;
}
$icon = ($type === 'picker') ? 'calendar' : 'remove';
Html::addCssClass($options, 'input-group-addon kv-date-' . $icon);
$icon = '';
$title = ArrayHelper::getValue($options, 'title', '');
if ($title !== false && empty($title)) {
$options['title'] = ($type === 'picker') ? Yii::t('kvdate', 'Select date') : Yii::t('kvdate', 'Clear field');
}
return Html::tag('span', $icon, $options);
}
/**
* Parses the input to render based on markup type
*
* @param string $input
* @return string
*/
protected function parseMarkup($input)
{
$css = $this->disabled ? ' disabled' : '';
if ($this->type == self::TYPE_INPUT || $this->type == self::TYPE_INLINE) {
if (isset($this->size)) {
Html::addCssClass($this->options, 'input-' . $this->size . $css);
}
} elseif ($this->type != self::TYPE_BUTTON && isset($this->size)) {
Html::addCssClass($this->_container, 'input-group input-group-' . $this->size . $css);
} elseif ($this->type != self::TYPE_BUTTON) {
Html::addCssClass($this->_container, 'input-group' . $css);
}
if ($this->type == self::TYPE_INPUT) {
return $input;
}
$part1 = $part2 = $part3 = $part4 = '';
if (!empty($this->addon) && ($this->_hasAddon || $this->type == self::TYPE_RANGE)) {
$part1 = ArrayHelper::getValue($this->addon, 'part1', '');
$part2 = ArrayHelper::getValue($this->addon, 'part2', '');
$part3 = ArrayHelper::getValue($this->addon, 'part3', '');
$part4 = ArrayHelper::getValue($this->addon, 'part4', '');
}
if ($this->_hasAddon) {
Html::addCssClass($this->_container, 'date');
$picker = $this->renderAddon($this->pickerButton);
$remove = $this->renderAddon($this->removeButton, 'remove');
if ($this->type == self::TYPE_COMPONENT_APPEND) {
$content = $part1 . $part2 . $input . $part3 . $remove . $picker . $part4;
} else {
$content = $part1 . $picker . $remove . $part2 . $input . $part3 . $part4;
}
return Html::tag('div', $content, $this->_container);
}
if ($this->type == self::TYPE_BUTTON) {
Html::addCssClass($this->_container, 'date');
$label = ArrayHelper::remove($this->buttonOptions, 'label', self::CALENDAR_ICON);
if (!isset($this->buttonOptions['disabled'])) {
$this->buttonOptions['disabled'] = $this->disabled;
}
if (empty($this->buttonOptions['class'])) {
$this->buttonOptions['class'] = 'btn btn-default';
}
$button = Html::button($label, $this->buttonOptions);
return Html::tag('div', "{$input}{$button}", $this->_container);
}
if ($this->type == self::TYPE_RANGE) {
Html::addCssClass($this->_container, 'input-daterange');
$this->initDisability($this->options2);
if (isset($this->form)) {
Html::addCssClass($this->options, 'form-control kv-field-from');
Html::addCssClass($this->options2, 'form-control kv-field-to');
$input = $this->form->field($this->model, $this->attribute, [
'template' => '{input}{error}',
'options' => ['class' => 'kv-container-from form-control'],
])->textInput($this->options);
$input2 = $this->form->field($this->model, $this->attribute2, [
'template' => '{input}{error}',
'options' => ['class' => 'kv-container-to form-control'],
])->textInput($this->options2);
} else {
if (empty($this->options2['id'])) {
$this->options2['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute2) : $this->getId() . '-2';
}
Html::addCssClass($this->options2, 'form-control');
$input2 = $this->hasModel() ?
Html::activeTextInput($this->model, $this->attribute2, $this->options2) :
Html::textInput($this->name2, $this->value2, $this->options2);
}
$content = $part1 . $input . $part2 . "{$this->separator}" .
$part3 . $input2 . $part4;
return Html::tag('div', $content, $this->_container);
}
if ($this->type == self::TYPE_INLINE) {
return Html::tag('div', '', $this->_container) . $input;
}
}
/**
* Registers the needed client assets
*/
public function registerAssets()
{
if ($this->disabled) {
return;
}
$view = $this->getView();
if (!empty($this->_langFile)) {
DatePickerAsset::register($view)->js[] = $this->_langFile;
} else {
DatePickerAsset::register($view);
}
$id = "jQuery('#" . $this->options['id'] . "')";
$el = "jQuery('#" . $this->options['data-datepicker-source'] . "')";
$this->registerPlugin($this->pluginName, $el);
if ($this->type === self::TYPE_INLINE) {
$view->registerJs("{$el}.on('changeDate',function(e){{$id}.val(e.format()).trigger('change')});");
}
if ($this->_hasAddon && $this->removeButton !== false) {
$view->registerJs("initDPRemove('" . $this->options['id'] . "');");
}
if ($this->_hasAddon && !empty($this->addon)) {
$view->registerJs("initDPAddon('" . $this->options['id'] . "');");
}
if ($this->type === self::TYPE_RANGE) {
\kartik\field\FieldRangeAsset::register($view);
$view->registerJs("initDPRemove('" . $this->options['id'] . "', true);");
}
}
}