1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403:
<?php
/**
* MvcCore
*
* This source file is subject to the BSD 3 License
* For the full copyright and license information, please view
* the LICENSE.md file that are distributed with this source code.
*
* @copyright Copyright (c) 2016 Tom Flidr (https://github.com/mvccore)
* @license https://mvccore.github.io/docs/mvccore/5.0.0/LICENCE.md
*/
namespace MvcCore\Ext\Forms\Fields;
/**
* Responsibility: init, pre-dispatch and render `<select>` HTML element
* as roll-out menu for single option select or as options
* list for multiple selection. `Select` field has it's own
* validator to check if submitted value is presented in
* configured options by default.
*/
class Select
extends \MvcCore\Ext\Forms\Field
implements \MvcCore\Ext\Forms\Fields\IVisibleField,
\MvcCore\Ext\Forms\Fields\ILabel,
\MvcCore\Ext\Forms\Fields\IMultiple,
\MvcCore\Ext\Forms\Fields\IOptions,
\MvcCore\Ext\Forms\Fields\IMinMaxOptions {
use \MvcCore\Ext\Forms\Field\Props\VisibleField;
use \MvcCore\Ext\Forms\Field\Props\Label;
use \MvcCore\Ext\Forms\Field\Props\AutoComplete;
use \MvcCore\Ext\Forms\Field\Props\Multiple;
use \MvcCore\Ext\Forms\Field\Props\Options;
use \MvcCore\Ext\Forms\Field\Props\MinMaxOptions;
use \MvcCore\Ext\Forms\Field\Props\NullOptionText;
use \MvcCore\Ext\Forms\Field\Props\Size;
/**
* MvcCore Extension - Form - Field - Selection - version:
* Comparison by PHP function version_compare();
* @see http://php.net/manual/en/function.version-compare.php
*/
const VERSION = '5.0.0';
/**
* Possible value: `select`, not used in HTML code for this field.
* @var string
*/
protected $type = 'select';
/**
* Possible values: a string value, array of strings (for select with `multiple` attribute) or `NULL`.
* @var string|array|NULL
*/
protected $value = NULL;
/**
* Validators:
* - `ValueInOptions` - to validate if submitted string(s)
* are presented in select options keys.
* @var string[]|\Closure[]
*/
protected $validators = ['ValueInOptions'];
/**
* Standard field template strings for natural
* rendering - `control`, `option` and `optionsGroup`.
* @var string
*/
protected static $templates = [
'control' => '<select id="{id}" name="{name}"{size}{attrs}>{options}</select>',
'option' => '<option value="{value}"{selected}{class}{attrs}>{text}</option>',
'optionsGroup' => '<optgroup{label}{class}{attrs}>{options}</optgroup>',
];
/**
* If select has `multiple` boolean attribute defined, this
* function returns `\string[]` array. If select has no `multiple`
* attribute, this function returns `string`.
* If there is no value selected or configured, function returns `NULL`.
* @return array|string|NULL
*/
public function GetValue () {
return $this->value;
}
/**
* If select has `multiple` boolean attribute, set to this
* function `\string[]` array. If select has not `multiple`
* attribute, set to this function `string`.
* If you don't want any selected value, set `NULL`.
* @param array|string|NULL $value
* @return \MvcCore\Ext\Forms\Fields\Select
*/
public function SetValue ($value) {
/** @var $this \MvcCore\Ext\Forms\Field */
$this->value = $value;
return $this;
}
/**
* Create new form `<select>` control instance.
* @param array $cfg Config array with public properties and it's
* values which you want to configure, presented
* in camel case properties names syntax.
* @throws \InvalidArgumentException
* @return void
*/
public function __construct(array $cfg = []) {
parent::__construct($cfg);
static::$templates = (object) array_merge(
(array) parent::$templates,
(array) self::$templates
);
}
/**
* This INTERNAL method is called from `\MvcCore\Ext\Form` after field
* is added into form instance by `$form->AddField();` method. Do not
* use this method even if you don't develop any form field.
* - Check if field has any name, which is required.
* - Set up form and field id attribute by form id and field name.
* - Set up required.
* - Set up translate boolean property.
* - Check if there are any select options in `$this->options`.
* - Set up select minimum/maximum options to select if necessary.
* @param \MvcCore\Ext\Form $form
* @throws \InvalidArgumentException
* @return \MvcCore\Ext\Forms\Fields\Select
*/
public function SetForm (\MvcCore\Ext\IForm $form) {
/** @var $this \MvcCore\Ext\Forms\Field */
parent::SetForm($form);
if (!$this->options) $this->throwNewInvalidArgumentException(
'No `options` property defined.'
);
// add minimum/maximum options count validator if necessary
$this->setFormMinMaxOptions();
return $this;
}
/**
* Return field specific data for validator.
* @param array $fieldPropsDefaultValidValues
* @return array
*/
public function & GetValidatorData ($fieldPropsDefaultValidValues = []) {
$result = [
'multiple' => $this->multiple,
'options' => & $this->options,
'minOptions' => $this->minOptions,
'maxOptions' => $this->maxOptions,
];
return $result;
}
/**
* This INTERNAL method is called from `\MvcCore\Ext\Form` just before
* field is naturally rendered. It sets up field for rendering process.
* Do not use this method even if you don't develop any form field.
* Set up field properties before rendering process.
* - Set up field render mode if not defined.
* - Translate label text if necessary.
* - Set up tab-index if necessary.
* - Translate all options if necessary, including null option text if necessary.
* @return void
*/
public function PreDispatch () {
parent::PreDispatch();
$this->preDispatchTabIndex();
if (!$this->translate) return;
$this->preDispatchNullOptionText();
if (!$this->translateOptions) return;
$form = $this->form;
foreach ($this->options as $key => & $value) {
if (is_scalar($value)) { // string|int|float|bool
// most simple key/value array options configuration
if ($value)
$options[$key] = $form->Translate((string)$value);
} else if (is_array($value)) {
if (isset($value['options']) && is_array($value['options'])) {
// `<optgroup>` options configuration
$this->preDispatchTranslateOptionOptGroup($value);
} else {
// advanced configuration with key, text, css class, and any other attributes for single option tag
$valueText = isset($value['text']) ? $value['text'] : $key;
if ($valueText) $value['text'] = $form->Translate((string) $valueText);
}
}
}
}
/**
* Translate select option item if option item is configured as array for option group.
* @param array & $optionsGroup
*/
protected function preDispatchTranslateOptionOptGroup (& $optionsGroup) {
$form = $this->form;
$groupLabel = isset($optionsGroup['label'])
? $optionsGroup['label']
: '';
if ($groupLabel)
$optionsGroup['label'] = $form->Translate((string) $groupLabel);
$groupOptions = $optionsGroup['options']
? $optionsGroup['options']
: [];
foreach ($groupOptions as $key => & $groupOption) {
if (is_scalar($groupOption)) {
// most simple key/value array options configuration
if ($groupOption)
$optionsGroup['options'][$key] = $form->Translate((string) $groupOption);
} else if (is_array($groupOption)) {
// advanced configuration with key, text, CSS class, and any other attributes for single option tag
$valueText = isset($groupOption['text']) ? $groupOption['text'] : $key;
if ($valueText) $groupOption['text'] = $form->Translate((string) $valueText);
}
}
}
/**
* This INTERNAL method is called from `\MvcCore\Ext\Forms\Field\Rendering`
* in rendering process. Do not use this method even if you don't develop any form field.
*
* Render control tag only without label or specific errors,
* including all select `<option>` tags or `<optgroup>` tags
* if there are options configured for.
* @return string
*/
public function RenderControl () {
$optionsStr = $this->RenderControlOptions();
$attrsStr = $this->renderControlAttrsWithFieldVars([
'autoComplete',
]);
if ($this->multiple) {
$attrsStr .= (strlen($attrsStr) > 0 ? ' ' : '')
. 'multiple="multiple"';
$name = $this->name . '[]';
$size = $this->size !== NULL ? ' size="' . $this->size . '"' : '';
} else {
$name = $this->name;
$size = '';
}
if (!$this->form->GetFormTagRenderingStatus())
$attrsStr .= (strlen($attrsStr) > 0 ? ' ' : '')
. 'form="' . $this->form->GetId() . '"';
$formViewClass = $this->form->GetViewClass();
/** @var $templates \stdClass */
$templates = static::$templates;
return $formViewClass::Format($templates->control, [
'id' => $this->id,
'name' => $name,
'size' => $size,
'options' => $optionsStr,
'attrs' => strlen($attrsStr) > 0 ? ' ' . $attrsStr : '',
]);
}
/**
* This INTERNAL method is called from `\MvcCore\Ext\Forms\Field\Rendering`
* in rendering process. Do not use this method even if you don't develop any form field.
*
* Render inner select control `<option>` tags or `<optgroup>`
* tags if there are options configured for.
* @return string
*/
public function RenderControlOptions () {
$result = '';
$valueTypeIsArray = is_array($this->value);
if ($this->nullOptionText !== NULL && mb_strlen((string) $this->nullOptionText) > 0) {
// advanced configuration with key, text, css class, and any other attributes for single option tag
$result .= $this->renderControlOptionsAdvanced(
NULL, [
'value' => NULL,
'text' => htmlspecialchars_decode(htmlspecialchars($this->nullOptionText, ENT_QUOTES), ENT_QUOTES),
//'attrs' => ['disabled' => 'disabled'] // this will cause the browser to select the first allowed option automatically
], $valueTypeIsArray
);
}
foreach ($this->options as $key => & $value) {
if (is_scalar($value)) {
// most simple key/value array options configuration
$result .= $this->renderControlOptionKeyValue($key, $value, $valueTypeIsArray);
} else if (is_array($value)) {
if (isset($value['options']) && is_array($value['options'])) {
// `<optgroup>` options configuration
$result .= $this->renderControlOptionsGroup($value, $valueTypeIsArray);
} else {
// advanced configuration with key, text, cs class, and any other attributes for single option tag
$result .= $this->renderControlOptionsAdvanced(
isset($value['value']) ? $value['value'] : $key, $value, $valueTypeIsArray
);
}
}
}
return $result;
}
/**
* Render select `<option>` tag with inner visible text and attributes: `value` and
* `selected` (optionally) by given `$value` string for value to select and `$text`
* string for visible text.
* @param string|NULL $value
* @param string $text
* @param bool $valueTypeIsArray
* @return string
*/
protected function renderControlOptionKeyValue ($value, & $text, $valueTypeIsArray) {
$selected = $valueTypeIsArray
? in_array($value, $this->value, TRUE)
: $this->value === $value ;
$formViewClass = $this->form->GetViewClass();
/** @var $templates \stdClass */
$templates = static::$templates;
return $formViewClass::Format($templates->option, [
'value' => htmlspecialchars_decode(htmlspecialchars($value, ENT_QUOTES), ENT_QUOTES),
'selected' => $selected ? ' selected="selected"' : '',
'text' => htmlspecialchars_decode(htmlspecialchars($text, ENT_QUOTES), ENT_QUOTES),
'class' => '', // to fill prepared template control place for attribute class with empty string
'attrs' => '', // to fill prepared template control place for other attributes with empty string
]);
}
/**
* Render `<optgroup>` tag including it's rendered `<option>` tags.
* @param array $optionsGroup
* @param bool $valueTypeIsArray
* @return string
*/
protected function renderControlOptionsGroup (& $optionsGroup, $valueTypeIsArray) {
$optionsStr = '';
foreach ($optionsGroup['options'] as $key => & $value) {
if (is_scalar($value)) {
// most simple key/value array options configuration
$optionsStr .= $this->renderControlOptionKeyValue($key, $value, $valueTypeIsArray);
} else if (is_array($value)) {
// advanced configuration with key, text, cs class, and any other attributes for single option tag
$optionsStr .= $this->renderControlOptionsAdvanced($key, $value, $valueTypeIsArray);
}
}
$label = isset($optionsGroup['label']) && strlen((string) $optionsGroup['label']) > 0
? $optionsGroup['label']
: NULL;
if (!$optionsStr && !$label) return '';
$formViewClass = $this->form->GetViewClass();
$classStr = isset($optionsGroup['class']) && strlen((string) $optionsGroup['class'])
? ' class="' . $optionsGroup['class'] . '"'
: '';
$attrsStr = isset($optionsGroup['attrs'])
? ' ' . $formViewClass::RenderAttrs($optionsGroup['attrs'])
: '';
/** @var $templates \stdClass */
$templates = static::$templates;
return $formViewClass::Format($templates->optionsGroup, [
'options' => $optionsStr,
'label' => ' label="' . $label. '"',
'class' => $classStr,
'attrs' => $attrsStr
]);
}
/**
* Render select `<option>` tag with inner visible text and attributes: `value`,
* `selected` (optionally), `class` (optionally) and any other optional attributes if configured
* by given `$value` string and `$optionData` array with additional option configuration data.
* @param string|NULL $value
* @param mixed $optionData
* @param mixed $valueTypeIsArray
* @return mixed
*/
protected function renderControlOptionsAdvanced ($value, $optionData, $valueTypeIsArray) {
$valueToRender = isset($optionData['value'])
? $optionData['value']
: ($value === NULL ? '' : $value);
if ($valueTypeIsArray) {
if (count($this->value) > 0) {
$selected = in_array($valueToRender, $this->value, TRUE);
} else {
$selected = $valueToRender === NULL;
}
} else {
$selected = $this->value === $valueToRender;
}
$formViewClass = $this->form->GetViewClass();
$classStr = isset($optionData['class']) && strlen((string) $optionData['class'])
? ' class="' . $optionData['class'] . '"'
: '';
$attrsStr = isset($optionData['attrs'])
? ' ' . $formViewClass::RenderAttrs($optionData['attrs'])
: '';
/** @var $templates \stdClass */
$templates = static::$templates;
return $formViewClass::Format($templates->option, [
'value' => htmlspecialchars_decode(htmlspecialchars($valueToRender, ENT_QUOTES), ENT_QUOTES),
'selected' => $selected ? ' selected="selected"' : '',
'class' => $classStr,
'attrs' => $attrsStr,
'text' => htmlspecialchars_decode(htmlspecialchars($optionData['text'], ENT_QUOTES), ENT_QUOTES),
]);
}
}