*
* Serves as the base for Inputfields that provide selection of options (whether single or multi).
* As a result, this class includes functionality for, and checks for both single-and-multi selection values.
* Sublcasses will want to override the render method, but it's not necessary to override processInput().
* Subclasses that select multiple values should implement the InputfieldHasArrayValue interface.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @property string|int $defaultValue
* @property array|string $options Get or set options, array of [value => label], or use options string.
* @property array $optionAttributes
* @property bool $valueAddOption If value attr set from API (only) that is not an option, add it as an option? (default=false) 3.0.171+
*
*/
class InputfieldSelect extends Inputfield implements InputfieldHasSelectableOptions {
/**
* Options specific to this Select
*
*/
protected $options = array();
/**
* Attributes for options specific to this select (if applicable)
*
*/
protected $optionAttributes = array();
/**
* Alternate language labels for options, array of [ languageID => [ optionValue => optionLabel ] ]
*
* @var array
*
*/
protected $optionLanguageLabels = array();
/**
* Return information about this module
*
*/
public static function getModuleInfo() {
return array(
'title' => __('Select', __FILE__), // Module Title
'summary' => __('Selection of a single value from a select pulldown', __FILE__), // Module Summary
'version' => 102,
'permanent' => true,
);
}
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->set('defaultValue', '');
$this->set('valueAddOption', false);
}
/**
* Add an option that may be selected
*
* If you want to add an optgroup, use the $value param as the label, and the label param as an array of options.
* Note that optgroups may not be applicable to other Inputfields that descend from InputfieldSelect.
*
* @param string $value Value that the option submits (or label of optgroup, if specifying an optgroup)
* @param string $label|array Optional label associated with the value (if null, value will be used as the label), or array of optgroup options [value=>label]
* @param array $attributes Optional attributes to be associated with this option (i.e. a 'selected' attribute for an ";
}
}
foreach($options as $value => $label) {
if(is_array($label)) {
$out .=
"";
continue;
}
$selected = $this->isOptionSelected($value) ? " selected='selected'" : '';
$attrs = $this->getOptionAttributes($value);
unset($attrs['selected'], $attrs['checked'], $attrs['value']);
$attrs = $this->getOptionAttributesString($attrs);
$out .=
"";
}
return $out;
}
/**
* Check for default value and populate when appropriate
*
* This should be called at the beginning of render() and at the end of processInput()
*
*/
protected function checkDefaultValue() {
if(!$this->required || !$this->defaultValue || !$this->isEmpty()) return;
// when a value is required and the value is empty and a default value is specified, we use it.
if($this instanceof InputfieldHasArrayValue) {
/** @var InputfieldSelect $this */
$value = explode("\n", $this->defaultValue);
foreach($value as $k => $v) {
$value[$k] = trim($v); // remove possible extra LF
}
} else {
$value = $this->defaultValue;
$pos = strpos($value, "\n");
if($pos) $value = substr($value, 0, $pos);
$value = trim($value);
}
$this->attr('value', $value);
}
/**
* Render ready
*
* @param Inputfield|null $parent
* @param bool $renderValueMode
* @return bool
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
if(!empty($this->optionLanguageLabels) && $this->hasFieldtype === false) {
$languages = $this->wire()->languages;
if($languages) {
// make option labels use use language where available
$language = $this->wire()->user->language;
$defaultLanguage = $languages->getDefault();
if(!empty($this->optionLanguageLabels[$language->id])) {
$labels = $this->optionLanguageLabels[$language->id];
foreach($this->options as $key => $defaultLabel) {
if(empty($labels[$key])) continue;
$this->options[$key] = $labels[$key];
if($language->id != $defaultLanguage->id) {
$this->optionLanguageLabel($defaultLanguage, $key, $defaultLabel);
}
}
}
}
}
return parent::renderReady($parent, $renderValueMode);
}
/**
* Render and return the output for this Select
*
* @return string
*
*/
public function ___render() {
$this->checkDefaultValue();
$attrs = $this->getAttributes();
unset($attrs['value']);
return
"";
}
/**
* Render non-editable value
*
* @return string
*
*/
public function ___renderValue() {
$out = '';
$sanitizer = $this->wire()->sanitizer;
foreach($this->options as $value => $label) {
$o = '';
if(is_array($label)) {
foreach($label as $k => $v) {
if($this->isOptionSelected($k)) {
$o = trim($value, ' :') . ": $v";
}
}
} else {
if($this->isOptionSelected($value)) $o = $label;
}
if(strlen($o)) {
$out .= "
" . $sanitizer->entities($o) . "
";
}
}
if(strlen($out)) {
$out = "
$out
";
}
return $out;
}
/**
* Process input from the provided array
*
* In this case we're having the Inputfield base process the input and we're going back and validating the value.
* If the value(s) that were set aren't in our specific list of options, we remove them. This is a security measure.
*
* @param WireInputData $input
* @return $this
*
*/
public function ___processInput(WireInputData $input) {
// disable valueAddOption temporarily to prevent it from applying to user input
$valueAddOption = $this->valueAddOption;
if($valueAddOption) $this->valueAddOption = false;
parent::___processInput($input);
$name = $this->attr('name');
if(!isset($input[$name])) {
$value = $this instanceof InputfieldHasArrayValue ? array() : null;
$this->setAttribute('value', $value);
return $this;
}
// validate that the selected posted option(s) are those from our options list
// removing any that aren't
$value = $this->attr('value');
if($this instanceof InputfieldHasArrayValue) {
/** @var InputfieldSelect $this */
if(!is_array($value)) $value = array();
foreach($value as $k => $v) {
if(!$this->isOption($v)) {
unset($value[$k]); // remove invalid option
}
}
} else if($value && !$this->isOption($value)) {
$value = null;
}
$this->setAttribute('value', $value);
$this->checkDefaultValue();
if($valueAddOption) $this->valueAddOption = $valueAddOption;
return $this;
}
/**
* Get property
*
* @param string $key
* @return array|mixed|null
*
*/
public function get($key) {
if($key === 'options') return $this->options;
if($key === 'optionAttributes') return $this->optionAttributes;
return parent::get($key);
}
/**
* Set property
*
* @param string $key
* @param mixed $value
* @return Inputfield|InputfieldSelect
*
*/
public function set($key, $value) {
if($key == 'options') {
if(is_string($value)) {
return $this->addOptionsString($value);
} else if(is_array($value)) {
$this->options = $value;
}
return $this;
} else if(strpos($key, 'options') === 0 && $this->hasFieldtype === false) {
list(,$languageID) = explode('options', $key);
if(ctype_digit($languageID)) {
$this->addOptionLabelsString($value, (int) $languageID);
return $this;
}
} else if($key == 'optionAttributes') {
if(is_array($value)) {
$this->optionAttributes = $value;
}
return $this;
}
return parent::set($key, $value);
}
/**
* Set attribute
*
* @param array|string $key
* @param array|int|string $value
* @return Inputfield|InputfieldSelect
*
*/
public function setAttribute($key, $value) {
if($key === 'value') {
if(is_object($value) || (is_string($value) && strpos($value, '|'))) {
$value = (string) $value;
if($this instanceof InputfieldHasArrayValue) {
$value = explode('|', $value);
}
} else if(is_array($value)) {
if($this instanceof InputfieldHasArrayValue) {
// ok
} else {
$value = reset($value);
}
}
if($this->valueAddOption) {
// add option(s) for any value set from API
if(is_array($value)) {
foreach($value as $v) {
if(!$this->isOption($v)) {
if(strlen($v)) $this->addOption($v);
}
}
} else {
if(strlen("$value") && !$this->isOption($value)) {
$this->addOption($value);
}
}
}
}
return parent::setAttribute($key, $value);
}
/**
* Is the value empty?
*
* @return bool
*
*/
public function isEmpty() {
/** @var array|null|bool|string|int $value */
$value = $this->attr('value');
if(is_array($value)) {
$cnt = count($value);
if(!$cnt) return true;
if($cnt === 1) return strlen((string) reset($value)) === 0;
return false; // $cnt > 1
} else if($value === null || $value === false) {
return true;
} else if("$value" === "0") {
if(!array_key_exists("$value", $this->options)) return true;
} else {
return strlen("$value") === 0;
}
return false;
}
/**
* Field configuration
*
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields() {
$inputfields = parent::___getConfigInputfields();
$modules = $this->wire()->modules;
if($this instanceof InputfieldHasArrayValue) {
/** @var InputfieldTextarea $f */
$f = $modules->get('InputfieldTextarea');
$f->description = $this->_('To have pre-selected default value(s), enter the option values (one per line) below.');
} else {
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->description = $this->_('To have a pre-selected default value, enter the option value below.');
}
$f->attr('name', 'defaultValue');
$f->label = $this->_('Default value');
$f->attr('value', $this->defaultValue);
$f->description .= ' ' . $this->_('For default page selection, the value would be the page ID number.');
$f->notes = $this->_('IMPORTANT: The default value is not used unless the field is required (see the “required” checkbox on this screen).');
$f->collapsed = $this->hasFieldtype === false ? Inputfield::collapsedBlank : Inputfield::collapsedNo;
$inputfields->add($f);
// if dealing with an inputfield that has an associated fieldtype,
// we don't need to perform the remaining configuration
if($this->hasFieldtype !== false) return $inputfields;
// the following configuration specific to non-Fieldtype use of single/multi-selects
$isInputfieldSelect = $this->className() == 'InputfieldSelect';
$languages = $this->wire()->languages;
/** @var InputfieldTextarea $f */
$f = $modules->get('InputfieldTextarea');
$f->attr('name', 'options');
$f->label = $this->_('Options');
$value = '';
foreach($this->options as $key => $option) {
if(is_array($option)) {
$value .= "$key\n";
foreach($option as $o) {
$value .= " $o\n";
}
} else {
$value .= "$option\n";
}
}
$value = trim($value);
if(empty($value)) {
$optionLabel = $f->label;
if($optionLabel === 'Options') $optionLabel = 'Option';
$value = "=\n$optionLabel 1\n$optionLabel 2\n$optionLabel 3";
if(!$isInputfieldSelect) $value = ltrim($value, '=');
}
$f->attr('value', $value);
$f->attr('rows', 10);
$f->description = $this->_('Enter the options that may be selected, one per line.');
if($languages) $f->description .= ' ' . $this->_('To use multi-language option labels, please see the instructions below this field.');
$f->notes =
($languages ? '**' . $this->_('Instructions:') . "**\n" : '') .
'• ' . $this->_('Specify one option per line.') . "\n" .
'• ' . $this->_('To keep a separate value and label, separate them with an equals sign. Example: value=My Option') . " \n" .
($isInputfieldSelect ? '• ' . $this->_('To precede your list with a blank option, enter just a equals sign "=" as the first option.') . "\n" : '') .
'• ' . $this->_('To make an option selected, precede it with a plus sign. Example: +My Option') .
($isInputfieldSelect ? "\n• " . $this->_('To create an optgroup (option group) indent the options in the group with 3 or more spaces.') : '');
if($languages) $f->notes .= " \n\n**" . $this->_('Multi-language instructions:') . "**\n" .
'• ' . $this->_('We recommend using using `value=label`, where `value` is the same across languages and `label` is translated.') . " \n" .
'• ' . $this->_('First define your default language options, and then copy/paste into the other languages and translate labels.') . " \n" .
'• ' . $this->_('Selected options and optgroups are defined on the default language; the other inputs are only for label translation.') . "\n" .
'• ' . $this->_('Labels that are not translated inherit the default language label.');
if($languages) {
$f->useLanguages = true;
}
$inputfields->add($f);
return $inputfields;
}
}