* * 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 2022 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 .= "" . $this->renderOptions($label, false) . ""; continue; } $selected = $this->isOptionSelected($value) ? " selected='selected'" : ''; $attrs = $this->getOptionAttributes($value); unset($attrs['selected'], $attrs['checked'], $attrs['value']); $attrs = $this->getOptionAttributesString($attrs); $out .= "" . $this->entityEncode($label) . ""; } 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 = ""; } 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() { $value = $this->attr('value'); if(is_array($value)) { $cnt = count($value); if(!$cnt) return true; if($cnt === 1) return strlen(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; } }