_defaultConfig['templates'] = [
//Used for button elements in button()
'button' => '',
//Wrapper container for checkboxes
'checkboxWrapper' => '
{{label}}
',
//Error message wrapper elements
'error' => '
{{content}}
',
//Container for error items
'errorList' => '
{{content}}
',
//Label element when inputs are not nested inside the label
'label' => '',
//Container element used by control()
'inputContainer' => '
{{content}}{{help}}
',
//Container element used by control() when a field has an error
'inputContainerError' => '
{{content}}{{error}}{{help}}
',
//Label element used for radio and multi-checkbox inputs
'nestingLabel' => '{{hidden}}{{input}}',
//Container for submit buttons
'submitContainer' => '
{{content}}
',
] + $this->_defaultConfig['templates'];
/**
* This value can be changed (via the `$_config` property) by the `create()`/`end()` methods
* @see https://getbootstrap.com/docs/5.3/forms/validation/#server-side
*/
$this->_defaultConfig['errorClass'] = 'is-invalid';
parent::__construct($view, $config);
}
/**
* @inheritDoc
*/
protected function _inputType(string $fieldName, array $options): string
{
$type = $options['type'] ?? parent::_inputType($fieldName, $options);
//Forces the `password` type if the current type is `text` and `$fieldName` contains "password" or "pwd" words
return $type == 'text' && (str_contains($fieldName, 'password') || str_contains($fieldName, 'pwd')) ? 'password' : $type;
}
/**
* @inheritDoc
*/
protected function _getLabel(string $fieldName, array $options)
{
if ($options['label'] === false) {
return false;
}
$label = is_string($options['label']) ? ['text' => $options['label']] : ($options['label'] ?? []);
/**
* Sets the label class.
* Checkboxes and radios always have their own `form-check-label` class, even in inline forms. Other input types,
* on inline forms, have the `visually-hidden` class. In all other cases, the default class is `form-label`.
* @todo what about `floatingInput` forms?
* @todo what about horizontal forms?
*/
$type = $this->_inputType($fieldName, $options);
if (in_array($type, ['checkbox', 'radio'])) {
$class = 'form-check-label';
} elseif ((empty($label['text']) && $type == 'ckeditor') || $this->isInline()) {
$class = 'visually-hidden';
}
$label = $this->addClass($label, $class ?? 'form-label');
if ($label['icon'] ?? false) {
$options['templateVars']['icon'] = $this->Icon->icon($label['icon'] . ' ');
unset($label['icon']);
}
return parent::_getLabel($fieldName, compact('label') + $options);
}
/**
* Checks if the currently created form is an inline form
* @return bool
*/
public function isInline(): bool
{
return $this->isInline;
}
/**
* @inheritDoc
*/
public function button(string $title, array $options = []): string
{
$options += ['escapeTitle' => false, 'icon' => null, 'templateVars' => [], 'type' => 'button'];
if ($options['icon']) {
$options['templateVars'] += ['icon' => $this->Icon->icon($options['icon']) . ' '];
unset($options['icon']);
}
return parent::button($title, $this->addButtonClasses($options));
}
/**
* Creates a CKEditor textarea
* @param string $fieldName This should be "modelname.fieldname"
* @param array $options Each type of input takes different options
* @return string
* @see \MeTools\View\Helper\LibraryHelper::ckeditor() to add the scripts for CKEditor
*/
public function ckeditor(string $fieldName, array $options = []): string
{
$options += ['type' => 'textarea'];
$options = $this->addClass($options, 'editor wysiwyg');
return parent::textarea($fieldName, $options);
}
/**
* Generates a form control element complete with label and wrapper div.
*
* ### Options:
*
* - `append-text` to append a text
* - `help` to add a help text
* - `prepend-text` to prepend a text
* @param string $fieldName This should be "modelname.fieldname"
* @param array $options Each type of input takes different options
* @return string Completed form widget
* @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-form-controls
* @see \Cake\View\Helper\FormHelper::control() for all available options
*/
public function control(string $fieldName, array $options = []): string
{
$options += ['escape' => false, 'help' => null, 'append-text' => null, 'prepend-text' => null, 'templates' => [], 'templateVars' => []];
$templateVars['divClass'] = 'mb-3 ';
if ($this->isInline()) {
$templateVars['divClass'] = 'col-12 ';
}
switch ($this->_inputType($fieldName, $options)) {
case 'checkbox':
$templateVars['divClass'] .= 'form-check ';
$class = 'form-check-input';
break;
case 'radio':
$options['templates'] += ['radioWrapper' => '
{{label}}
'];
$class = 'form-check-input';
break;
case 'select':
$class = 'form-select';
break;
case 'time':
$options += ['step' => 60];
break;
}
$options = $this->addClass($options, $class ?? 'form-control');
/**
* Help text (form text).
* These are ignored in inline forms.
* @see https://getbootstrap.com/docs/5.3/forms/form-control/#form-text
* @todo Form text should be explicitly associated with the form control it relates to using the aria-labelledby
* (for mandatory information such as data format) or aria-describedby (for complementary information) attribute
*/
if ($options['help'] && !$this->isInline()) {
$help = array_map(fn(string $help): string => $this->Html->div('form-text', trim($help)), (array)$options['help']);
$templateVars['help'] = implode('', $help);
}
/**
* Input group (`append-text` and `prepend-text` options).
* It can also handle buttons.
* @see https://getbootstrap.com/docs/5.3/forms/input-group
*/
if ($options['append-text'] || $options['prepend-text']) {
foreach (['append', 'prepend'] as $name) {
$value = $options[$name . '-text'];
//Buttons and submits are not wrapped in a `span` element
if ($value && !str_starts_with($value, '