1: <?php
2: namespace TIC\FormBundle\Base;
3:
4: use Symfony\Component\Form\AbstractType;
5: #use Symfony\Component\Form\Extension\Core\Type\FormType;
6: #use Symfony\Component\Form\Util\StringUtil;
7: use Symfony\Component\Form\FormInterface;
8: use Symfony\Component\Form\FormView;
9: use Symfony\Component\OptionsResolver\OptionsResolver;
10: use Symfony\Component\OptionsResolver\Options;
11:
12: /**
13: * Classe parente pour tous les widgets fournis par le bundle.
14: */
15: abstract class TICWidgetType extends AbstractType
16: {
17: /**
18: * ATTENTION: une propriété doit être vue comme une constante de classe !
19: * Sa valeur sera commune aux différentes pseudo-instances des FormType.
20: * Ne pas s'en servir pour stocker des données dépendantes des options.
21: */
22: protected $form_alias = null;
23: protected $form_parent = null;
24: protected $default_prefix = '<chevron-right>';
25: protected $default_suffix = '<pencil>';
26: protected $class_prefix = 'input-group-text';
27: protected $class_suffix = 'input-group-text';
28: protected $soft_required = null;
29:
30:
31: /**
32: * Initialisation du $form_alias.
33: */
34: public function __construct()
35: {
36: if ($this->form_alias === null) {
37: $this->form_alias = 'tic_widget';
38: if (preg_match("/^TIC\\\\.*\\\\([^\\\\]+)Type\$/", static::class, $match)) {
39: # $this->form_alias = "tic_" . strtolower($match[1]);
40: $this->form_alias = "tic_" . strtolower(trim(preg_replace("/([A-Z])/", '_\1', $match[1]), '_'));
41: }
42: }
43: }
44:
45:
46: /**
47: * {@inheritdoc}
48: */
49: public function getParent(): string
50: {
51: if ($this->form_parent !== null) return $this->form_parent;
52: return parent::getParent(); // FormType::class;
53: }
54:
55:
56: /**
57: * {@inheritdoc}
58: */
59: public function getBlockPrefix(): string
60: {
61: if ($this->form_alias !== null) return $this->form_alias;
62: return parent::getBlockPrefix(); // StringUtil::fqcnToBlockPrefix(static::class) ?: '';
63: }
64:
65:
66: /**
67: * {@inheritdoc}
68: */
69: public function configureOptions(OptionsResolver $resolver): void
70: {
71: $resolver->setDefaults(array(
72: 'label' => true,
73: 'label_attr' => array(),
74: 'placeholder' => false,
75: 'pattern' => null, // regexp de validation HTML5
76: 'prefix' => null, // traduisible, html autorisé, raccourci <glyph-name>
77: 'suffix' => null, // traduisible, html autorisé, raccourci <glyph-name>
78: 'prefix_attr' => array(),
79: 'suffix_attr' => array(),
80: 'group_class' => null, // classes à ajouter sur le conteneur du widget
81: 'row_class' => null, // classes à ajouter sur le conteneur principal du form type
82: 'grid_class' => null, // bootstrap grid pour le row_class : "12,6,4" ou "xs-12,sm-6,md-4"
83: 'inline' => false, // bootstrap form-inline sans cols pour label/widget
84: 'sizing' => null, // bootstrap form sizing : sm | md | lg (héritage si null)
85: 'required' => true,
86: 'readonly' => false,
87: 'strict' => false, // traitement normal sans tolérance sur des données sources de type invalide
88: ));
89:
90: $resolver->setAllowedTypes('label_attr', array('array'));
91: $resolver->setAllowedTypes('placeholder', array('null', 'string', 'bool'));
92: $resolver->setAllowedTypes('pattern', array('null', 'string', 'bool'));
93: $resolver->setAllowedTypes('prefix', array('null', 'string', 'bool'));
94: $resolver->setAllowedTypes('suffix', array('null', 'string', 'bool'));
95: $resolver->setAllowedTypes('prefix_attr', array('array'));
96: $resolver->setAllowedTypes('suffix_attr', array('array'));
97: $resolver->setAllowedTypes('readonly', array('bool'));
98: $resolver->setAllowedTypes('group_class', array('null', 'string'));
99: $resolver->setAllowedTypes('row_class', array('null', 'string'));
100: $resolver->setAllowedTypes('grid_class', array('null', 'string'));
101: $resolver->setAllowedTypes('inline', array('bool'));
102: $resolver->setAllowedTypes('strict', array('bool'));
103: $resolver->setAllowedValues('sizing', array(null, 'sm', 'md', 'lg'));
104: $resolver->setAllowedValues('required', array(true, false, 'soft'));
105:
106: // gestion du "soft-required" : required passé à false mais label_attr avec classe 'required'
107: $resolver->setNormalizer('required', function (Options $options, $required) {
108: // reinit à false important car la propriété est partagée par tous les widgets d'un même FormType
109: $this->soft_required = false;
110: if ($required === 'soft') { $this->soft_required = true; $required = false; }
111: return $required;
112: });
113: $resolver->setNormalizer('label_attr', function (Options $options, $label_attr) {
114: if ($options['required'] === false && $this->soft_required) {
115: $label_attr['class'] = isset($label_attr['class']) ? $label_attr['class'] . ' required' : 'required';
116: }
117: return $label_attr;
118: });
119:
120: // gestion de l'option 'column' en ajoutant les classes nécessaires à 'row_class'
121: $resolver->setNormalizer('row_class', function (Options $options, $row_class) {
122: if ($options['grid_class'] !== null) {
123: $row_class = empty($row_class) ? 'form-group-col' : $row_class.' form-group-col';
124: $formats = array('xs','sm','md','lg');
125: foreach (explode(',', $options['grid_class']) as $idx => $col) {
126: if ($col === '') continue;
127: if (ctype_digit($col)) $col = $formats[$idx] . '-' . $col;
128: $row_class.= ' col-' . $col;
129: }
130: }
131: return $row_class;
132: });
133: }
134:
135:
136: /**
137: * {@inheritdoc}
138: */
139: public function buildView(FormView $view, FormInterface $form, array $options): void
140: {
141: // input_group avec prefix/suffix ?
142: $this->setViewInputGroup($view, $options);
143:
144: // héritage de l'option sizing (utile avec tic_collection)
145: while ($options['sizing'] === null) {
146: $parent = isset($parent) ? $parent->getParent() : $form->getParent();
147: if (! isset($parent)) break;
148: $options['sizing'] = $parent->getConfig()->getOption('sizing');
149: }
150: $view->vars['sizing'] = ($options['sizing'] === null) ? 'md' : $options['sizing'];
151:
152: // class form-inline sur le conteneur et affichage des enfants avec form_row sans cols
153: $view->vars['inline'] = ($options['inline'] && $options['compound']) ? true : false;
154:
155: // ajout des options placeholder et pattern dans les attributs du champs
156: if (! empty($options['placeholder'])) $view->vars['attr']['placeholder'] = $options['placeholder'];
157: if (! empty($options['pattern'])) $view->vars['attr']['pattern'] = $options['pattern'];
158:
159: // définition des classes à appliquer sur les conteneurs du widget
160: // Note: dépend des variables 'inline', 'input_group' et 'sizing'
161: $this->setViewGroupClass($view, $options);
162:
163: // spécification de la taille sur le widget lui-même (ajout de classe bootstrap)
164: // Note: dépend de la variable 'sizing'
165: $this->setViewInputSizing($view, $options);
166:
167: // options readonly propre au widget
168: $this->setViewReadOnly($view, $options);
169: }
170:
171:
172: /**
173: *
174: */
175: protected function generateContent($value, $default)
176: {
177: if ($value === true ) $value = $default;
178: if ($value === null ) return;
179: if ($value === false) return;
180: $value = preg_replace('/<fa([srlb]?)\-([\w\-]+)>/', '<i class="fa${1} fa-${2}" aria-hidden="true"></i>', $value);
181: $value = preg_replace('/<bi\-([\w\-]+)>/', '<i class="bi bi-${1}" aria-hidden="true"></i>', $value);
182: $value = preg_replace('/<gi\-([\w\-]+)>/', '<span class="glyphicon glyphicon-${1}" aria-hidden="true"></span>', $value);
183: # $value = preg_replace( '/^<([\w\-]+)>$/', '<span class="glyphicon glyphicon-${1}" aria-hidden="true"></span>', $value);
184: $value = preg_replace( '/^<([\w\-]+)>$/', '<i class="bi bi-${1}" aria-hidden="true"></i>', $value);
185: $value = preg_replace('/^\[([^\|\]]+)\|([\w\-\s]+)(\|([^\|\]]*))?\]$/', '<button class="btn btn-${2}" type="button" title="${4}">${1}</button>', $value);
186: return $value;
187: }
188:
189: /**
190: * Définition des éventuels contenus préfixes & suffixes du widget.
191: */
192: protected function setViewInputGroup(FormView $view, $options = array()): void
193: {
194: if (! isset($view->vars['input_group'])) $view->vars['input_group'] = false;
195:
196: // définition du contenu et des attributs du préfixe du champs
197: $view->vars['prefix'] = $this->generateContent($options['prefix'], $this->default_prefix);
198: if ($view->vars['prefix'] !== null) {
199: $view->vars['prefix_attr'] = $options['prefix_attr'] + array('class' => '');
200: $class = strpos($view->vars['prefix'], 'btn btn') ? 'input-group-btn' : $this->class_prefix;
201: $view->vars['prefix_attr']['class'] = trim($view->vars['prefix_attr']['class'].' '.$class);
202: $view->vars['input_group'] = true;
203: }
204:
205: // définition du contenu et des attributs du suffixe du champs
206: $view->vars['suffix'] = $this->generateContent($options['suffix'], $this->default_suffix);
207: if ($view->vars['suffix'] !== null) {
208: $view->vars['suffix_attr'] = $options['suffix_attr'] + array('class' => '');
209: $class = strpos($view->vars['suffix'], 'btn btn') ? 'input-group-btn' : $this->class_suffix;
210: $view->vars['suffix_attr']['class'] = trim($view->vars['suffix_attr']['class'].' '.$class);
211: $view->vars['input_group'] = true;
212: }
213: }
214:
215: /**
216: * Définition des classes à appliquer sur les conteneurs du widget (variables 'row_class' et 'group_class').
217: * Note: dépend des variables 'inline', 'input_group' et 'sizing'
218: */
219: protected function setViewGroupClass(FormView $view, $options = array()): void
220: {
221: $view->vars['row_class'] = $options['row_class'];
222: $view->vars['group_class'] = strtr($this->form_alias, '_', '-') . '-widget';
223:
224: if (isset($options['group_class'])) {
225: $view->vars['group_class'].= ' ' . $options['group_class'];
226: }
227: if ($view->vars['inline']) {
228: $view->vars['group_class'].= ' form-inline';
229: }
230: if ($view->vars['input_group']) {
231: $view->vars['group_class'].= ' input-group';
232: if ($view->vars['sizing'] !== null) $view->vars['group_class'].= ' input-group-' . $view->vars['sizing'];
233: }
234: }
235:
236: /**
237: * Définition de la classe à appliquer pour la taille du widget (classe bootstrap).
238: * Note: dépend de la variable 'sizing'
239: */
240: protected function setViewInputSizing(FormView $view, $options = array()): void
241: {
242: if ($view->vars['sizing'] !== null) {
243: $class = isset($view->vars['attr']['class']) ? $view->vars['attr']['class'] . ' ' : '';
244: $view->vars['attr']['class'] = $class . 'input-' . $view->vars['sizing'];
245: }
246: }
247:
248: /**
249: * Définition de l'état "lecture seule" du widget (variable et attribut HTML).
250: * Note: compatibilité ave l'option 'read_only' (deprecated)
251: */
252: protected function setViewReadOnly(FormView $view, $options = array()): void
253: {
254: $view->vars['readonly'] = $options['readonly']
255: || (array_key_exists('read_only', $options) && $options['read_only'])
256: || (array_key_exists('readonly', $options['attr']) && $options['attr']['readonly'])
257: ;
258: if ($view->vars['readonly']) $view->vars['attr']['readonly'] = true;
259: }
260:
261: }
262: