1: <?php
2: namespace TIC\FormBundle\Form\Type;
3:
4: #use Symfony\Component\Form\AbstractType as BaseType;
5: use TIC\FormBundle\Base\TICWidgetType as BaseType;
6:
7: use Symfony\Component\OptionsResolver\OptionsResolver;
8: use Symfony\Component\OptionsResolver\Options;
9: use Symfony\Component\Form\FormBuilderInterface;
10: use Symfony\Component\Form\FormInterface;
11: use Symfony\Component\Form\FormView;
12:
13: /**
14: * Sélecteur de date avec horaire (popup avec calendrier).
15: * Intégration du composant JS bootstrap-datetimepicker
16: */
17: class DatetimeType extends BaseType
18: {
19: protected $form_parent = \Symfony\Component\Form\Extension\Core\Type\DateTimeType::class;
20: protected $default_prefix = '<calendar>';
21: protected $default_suffix = '<calendar>';
22: /** Formattage standard forcé coté PHP (transformations selon la locale laissées au composant JS) */
23: const INTERNAL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
24:
25:
26: /**
27: * {@inheritdoc}
28: */
29: public function configureOptions(OptionsResolver $resolver): void
30: {
31: parent::configureOptions($resolver);
32:
33: $resolver->setDefaults(array(
34: 'placeholder' => false, // true, false, String (ex: "widget.date.ph_format_dt")
35: 'inline' => true,
36: 'suffix' => true,
37: 'minDate' => null,
38: 'maxDate' => null,
39:
40: 'with_js' => true, // utilise le Bootstrap DateTimePicker (défaut), sinon champs HTML5 Date+Time
41: 'html5' => false,
42: 'widget' => 'single_text',
43: 'format' => self::INTERNAL_DATE_FORMAT,
44: 'immutable' => false,
45:
46: 'jsformat' => 'L LT', // cf http://momentjs.com/docs/#/displaying/format/
47: 'culture' => \Locale::getPrimaryLanguage(\Locale::getDefault()),
48: 'viewDate' => null, // date par défaut du sélecteur (mais non sélectionnée)
49: 'viewMode' => 'days', // 'days', 'months', 'years' or 'decades'
50: 'sideTime' => true,
51: 'buttons' => true, // array('today','clear','close') | true=ALL | false=NONE
52: ));
53:
54: $resolver->setAllowedTypes('immutable', array('bool'));
55: $resolver->setAllowedTypes('with_js', array('bool'));
56: $resolver->setAllowedTypes('jsformat', array('string'));
57: $resolver->setAllowedTypes('viewDate', array('null', 'datetime', 'timestamp', 'int', 'string'));
58: $resolver->setAllowedTypes('viewMode', array('string'));
59: $resolver->setAllowedTypes('sideTime', array('bool'));
60: $resolver->setAllowedTypes('minDate', array('null', 'datetime', 'timestamp', 'int', 'string', 'boolean'));
61: $resolver->setAllowedTypes('maxDate', array('null', 'datetime', 'timestamp', 'int', 'string', 'boolean'));
62:
63: $resolver->setAllowedValues('viewMode', array('days', 'months', 'years', 'decades'));
64:
65: $resolver->setNormalizer('input', function (Options $options, $input) {
66: return $options['immutable'] ? 'datetime_immutable' : 'datetime';
67: });
68: // option html5 forcée si picker JS désactivé
69: $resolver->setNormalizer('html5', function (Options $options, $html5) {
70: return ($options['with_js']) ? false : true;
71: });
72: // option widget annulée si mode html5 (force l'usage du double widget date+time)
73: $resolver->setNormalizer('widget', function (Options $options, $widget) {
74: return ($options['html5']) ? null : 'single_text';
75: });
76: // option minDate avec utilisation de la date actuelle si true
77: $resolver->setNormalizer('minDate', function (Options $options, $minDate) {
78: if (is_bool($minDate)) $minDate = ($minDate) ? new \DateTime() : null;
79: return $minDate;
80: });
81: // option maxDate avec utilisation de la date actuelle si true
82: $resolver->setNormalizer('maxDate', function (Options $options, $maxDate) {
83: if (is_bool($maxDate)) $maxDate = ($maxDate) ? new \DateTime() : null;
84: return $maxDate;
85: });
86: // option buttons (DateTimePicker) avec listes par défaut si true/false
87: $resolver->setNormalizer('buttons', function (Options $options, $buttons) {
88: if (is_bool($buttons)) $buttons = ($buttons) ? array('today', 'clear', 'close') : array();
89: return $buttons;
90: });
91: // annulation du normalizer appliqué dans le champs parent DateTimeType
92: $resolver->setNormalizer('placeholder', function (Options $options, $value) { return $value; });
93: }
94:
95:
96: /**
97: * {@inheritdoc}
98: */
99: public function buildForm(FormBuilderInterface $builder, array $options): void
100: {
101: // reconfiguration du mode bi-widget (date+time) pour utiliser les TIC widgets
102: if ('single_text' !== $options['widget']) {
103:
104: // définition des options communes
105: $child_options = array(
106: 'with_js' => $options['with_js'],
107: 'html5' => $options['html5'],
108: 'widget' => 'single_text',
109: 'input' => 'array',
110: 'label' => false,
111: 'error_bubbling' => true,
112: 'prefix' => $options['prefix'],
113: 'suffix' => $options['suffix'],
114: # 'sizing' => $options['sizing'],
115: 'placeholder' => $options['placeholder'],
116: 'readonly' => $options['readonly'],
117: 'required' => $options['required'],
118: );
119:
120: // when the form is compound the entries of the array are ignored in favor of children data
121: // so we need to handle the cascade setting here
122: $emptyData = $builder->getEmptyData() ?: [];
123: // Only pass a subset of the options to children
124: if ($emptyData instanceof \Closure) {
125: $lazyEmptyData = static function ($option) use ($emptyData) {
126: return static function (FormInterface $form) use ($emptyData, $option) {
127: $emptyData = $emptyData($form->getParent());
128: return isset($emptyData[$option]) ? $emptyData[$option] : '';
129: };
130: };
131: $child_options['empty_data'] = $lazyEmptyData('date');
132: } elseif (isset($emptyData['date'])) {
133: $child_options['empty_data'] = $emptyData['date'];
134: }
135:
136: // construction des deux widgets date+time
137: $builder
138: ->add('date', DatePickerType::class, $child_options + array(
139: 'minDate' => $options['minDate'],
140: 'maxDate' => $options['maxDate'],
141: # 'format' => $options['date_format'],
142: ))
143: ->add('time', TimePickerType::class, $child_options + array(
144: 'with_minutes' => $options['with_minutes'],
145: 'with_seconds' => $options['with_seconds'],
146: ))
147: ;
148: }
149: }
150:
151:
152: /**
153: * {@inheritdoc}
154: */
155: public function finishView(FormView $view, FormInterface $form, array $options): void
156: {
157: // valeurs min/max avec correction éventuelle de cohérence
158: $view->vars['minDate'] = $this->readDateOption($options['minDate']);
159: $view->vars['maxDate'] = $this->readDateOption($options['maxDate']);
160: if (($view->vars['minDate'] !== null) && ($view->vars['maxDate'] !== null)) {
161: if (strcmp($view->vars['maxDate'], $view->vars['minDate'])<0) $view->vars['maxDate'] = $view->vars['minDate'];
162: }
163:
164: // valeurs utilisées uniquement en mode JS par le Bootstrap DateTimePicker
165: $view->vars['with_js'] = $options['with_js'];
166: if ($options['with_js']) {
167: $view->vars['format'] = $options['jsformat'];
168: $view->vars['culture'] = $options['culture'];
169: $view->vars['sideTime'] = $options['sideTime'];
170: $view->vars['buttons'] = $options['buttons'];
171: $view->vars['viewMode'] = $options['viewMode'];
172: $view->vars['viewDate'] = $this->readDateOption($options['viewDate'], true);
173:
174: if ($view->vars['minDate'] !== null) {
175: if (($view->vars['value'] !== null) && (strcmp($view->vars['value'], $view->vars['minDate'])<0)) $view->vars['value'] = null;
176: if (($view->vars['viewDate'] !== null) && (strcmp($view->vars['viewDate'],$view->vars['minDate'])<0)) $view->vars['viewDate']= $view->vars['minDate'];
177: }
178: if ($view->vars['maxDate'] !== null) {
179: if (($view->vars['value'] !== null) && (strcmp($view->vars['value'], $view->vars['maxDate'])>0)) $view->vars['value'] = null;
180: if (($view->vars['viewDate'] !== null) && (strcmp($view->vars['viewDate'],$view->vars['maxDate'])>0)) $view->vars['viewDate']= $view->vars['maxDate'];
181: }
182: $view->vars['jsValue'] = empty($view->vars['value']) ? 'false' : 'new Date("'.$view->vars['value'].'")';
183: }
184:
185: // spécification de la taille du widget (ajout de classe bootstrap)
186: $this->setViewInputSizing($view);
187: }
188:
189:
190: /**
191: * Conversion d'un paramètre de date.
192: * @param mixed $dateval Valeur timestamp | objet Datetime | chaine ISO 8601
193: * @param string $default Valeur par défaut si $dateval est nul
194: * @return string Date dans une chaine au format ISO 8601
195: */
196: protected function readDateOption($dateval, $default = null): ?string
197: {
198: if ($dateval === null) return ($default === true) ? date('c') : $default;
199: if (is_numeric($dateval)) return date('c', $dateval);
200: if (is_object($dateval)) return $dateval->format('c');
201: return $dateval;
202: }
203:
204:
205: }
206: