get('InputfieldToggle'); * $f->attr('name', 'test_toggle_field'); * $f->label = 'Do you like toggle fields?'; * * // Optionally make it show "On" and "Off" (rather than "Yes" and "No"): * $f->labelType = InputfieldToggle::labelTypeOn; * * // Optionally set custom labels: * $f->labelType = InputfieldToggle::labelTypeCustom; * $f->yesLabel = 'Yes please'; * $f->noLabel = 'No thanks'; * * // Optionally add an "other" option with label "Not sure": * $f->useOther = true; * $f->otherLabel = 'Not sure'; * * // Set the value: 0=No, 1=Yes, 2=Other, or blank string '' for no selection (Unknown) * $f->val(1); * * // Optionally set to use radio buttons rather than toggle buttons: * $f->inputfieldClass = 'InputfieldRadios'; * * // Remember to add to your InputfieldForm, InputfieldWrapper or InputfieldFieldset: * $form->add($f); * echo $form->render(); * ~~~~~~ * * @property int|string $value Integer value when selection is made or blank string when no selection (0=No, 1=Yes, 2=Other, ''=Unknown) * @property int $labelType Label type to use, see the labelType constants (default=labelTypeYes) * @property int $valueType Type of value for methods that ask for it (use one of the valueType constants) * @property string $yesLabel Custom yes/on label * @property string $noLabel Custom no/off label * @property string $otherLabel Custom label for optional other value Label to use for "other" option * @property int|bool $useReverse Reverse the order of the Yes/No options? (default=false) * @property int|bool $useOther Use the "other" option? (default=false) * @property bool|int $useVertical Use vertically oriented radio buttons? Applies only if $inputfieldClass is 'InputfieldRadios' (default=false) * @property bool|int $useDeselect Allow radios or toggles to be de-selected, enabling possibility of no-selection? (default=false) * @property string $defaultOption Default selected value of 'no', 'yes', 'other' or 'none' (default='none') * @property string $inputfieldClass Inputfield class to use or blank for this toggle buttons (default='') * * @method InputfieldSelect|InputfieldRadios getInputfield() * */ class InputfieldToggle extends Inputfield { public static function getModuleInfo() { return array( 'title' => __('Toggle', __FILE__), 'summary' => __('A toggle providing similar input capability to a checkbox but much more configurable.', __FILE__), 'version' => 1, ); } // label type constants const labelTypeYes = 0; const labelTypeTrue = 1; const labelTypeOn = 2; const labelTypeEnabled = 3; const labelTypeCustom = 100; // value constants const valueNo = 0; const valueYes = 1; const valueOther = 2; const valueUnknown = ''; /** * Array of all label types * * @var array * */ protected $labelTypes = array( 'yes' => self::labelTypeYes, 'true' => self::labelTypeOn, 'on' => self::labelTypeTrue, 'enabled' => self::labelTypeEnabled, 'custom' => self::labelTypeCustom, ); /** * Array of all value types * * @var array * */ protected $valueTypes = array( 'no' => self::valueNo, 'yes' => self::valueYes, 'other' => self::valueOther, 'unknown' => self::valueUnknown ); /** * Deleted Inputfield object for rendering (InputfieldRadios, InputfieldSelect, etc.) * * @var InputfieldSelect|InputfieldRadios Or any that extends them and does not have array value * */ protected $inputfield = null; /** * Cached result of a getAllLabels() call * * @var array * */ protected $allLabels = array(); /** * Manually added custom options of [ value => label ] * * @var array * */ protected $customOptions = array(); /** * Construct and set default settings * */ public function __construct() { $this->set('labelType', self::labelTypeYes); $this->set('yesLabel', '✓'); $this->set('noLabel', '✗'); $this->set('otherLabel', $this->_('?')); $this->set('useOther', 0); $this->set('useReverse', 0); $this->set('useVertical', 0); $this->set('useDeselect', 0); $this->set('defaultOption', 'none'); $this->set('inputfieldClass', '0'); $this->set('settings', array( 'inputCheckedClass' => '', 'labelCheckedClass' => '', )); $this->attr('value', self::valueUnknown); parent::__construct(); } public function wired() { $languages = $this->wire('languages'); if($languages) { foreach($languages as $language) { if($language->isDefault()) continue; $this->set("yesLabel$language", ''); $this->set("noLabel$language", ''); $this->set("otherLabel$language", ''); } } parent::wired(); } /** * Is the current value empty? (i.e. no selection) * * @return bool * */ public function isEmpty() { $value = $this->val(); if($value === self::valueUnknown) return true; if(is_int($value)) { if($this->hasCustomOptions()) { if(isset($this->customOptions[$value])) return false; } else { if($value > -1) return false; } } else if($value && $value !== 'unknown' && isset($this->valueTypes[$value])) { return false; } if($value === self::valueOther && $this->useOther) return false; return true; } /** * Sanitize the value to be one ofthe constants: valueYes, valueNo, valueOther, valueUnknown * * @param string|int $value Value to sanitize * @param bool $getName Get internal name of value rather than value? (default=false) * @return int|string * */ public function sanitizeValue($value, $getName = false) { if($value === null) { return $getName ? 'unknown' : self::valueUnknown; } if(is_bool($value)) { if($getName) return $value ? 'yes' : 'no'; return $value ? self::valueYes : self::valueNo; } $intValue = strlen("$value") && ctype_digit("$value") ? (int) "$value" : ''; $strValue = strtolower("$value"); if($this->hasCustomOptions()) { if($intValue !== '') $value = $intValue; $value = isset($this->customOptions[$value]) ? $value : self::valueUnknown; } else if($intValue === self::valueNo || $intValue === self::valueYes) { $value = $intValue; } else if($intValue === self::valueOther) { $value = $intValue; } else if($strValue === 'yes' || $strValue === 'on' || $strValue === 'true') { $value = self::valueYes; } else if($strValue === 'no' || $strValue === 'off' || $strValue === 'false') { $value = self::valueNo; } else if($strValue === 'unknown' || $strValue === '') { $value = self::valueUnknown; } else if(is_string($value) && strlen($value)) { // attempt to match to a label $value = null; foreach($this->getAllLabels() as $key => $label) { if(strtolower($label) !== $strValue) continue; list($labelType, $valueType, $languageName) = explode(':', $key); if($labelType || $languageName) {} // ignore $value = $this->valueTypes[$valueType]; break; } if($value === null) $value = self::valueUnknown; } else { $value = self::valueUnknown; // blank string } if($getName && !$this->hasCustomOptions()) { if($value === self::valueUnknown) { $value = 'unknown'; } else if($value === self::valueYes) { $value = 'yes'; } else if($value === self::valueNo) { $value = 'no'; } else if($value === self::valueOther) { $value = 'other'; } } return $value; } /** * Set attribute * * @param array|string $key * @param array|bool|int|string $value * @return Inputfield * */ public function setAttribute($key, $value) { if($key === 'value') $value = $this->sanitizeValue($value); return parent::setAttribute($key, $value); } /** * Get the delegated Inputfield that will be used for rendering selectable options * * @return InputfieldRadios|InputfieldSelect|InputfieldToggle * */ public function ___getInputfield() { if($this->inputfield) return $this->inputfield; $class = $this->getSetting('inputfieldClass'); if(empty($class) || $class === $this->className()) { if(false && $this->wire('adminTheme') == 'AdminThemeDefault') { // clicking toggles jumps to top of page on AdminThemeDefault for some reason // even if JS click events are canceled, so use radios instead $class = 'InputfieldRadios'; } else { return $this; } } $f = $this->wire('modules')->get($class); if(!$f || $f === $this) return $this; $this->addClass($class, 'wrapClass'); /** @var InputfieldSelect|InputfieldRadios $f */ $f->attr('name', $this->attr('name')); $f->attr('id', $this->attr('id')); $f->addClass($this->attr('class')); if(!$this->useVertical) { $f->set('optionColumns', 1); } $val = $this->val(); $options = $this->getOptions(); $f->addOptions($options); if(isset($options[$val]) && method_exists($f, 'addOptionAttributes')) { $f->addOptionAttributes($val, array('input.class' => 'InputfieldToggleChecked')); } $f->val($val); $this->inputfield = $f; return $f; } /** * Render ready * * @param Inputfield|null $parent * @param bool $renderValueMode * @return bool * */ public function renderReady(Inputfield $parent = null, $renderValueMode = false) { $f = $this->getInputfield(); if($f && $f !== $this) $f->renderReady($parent, $renderValueMode); if($this->useDeselect && $this->defaultOption === 'none') { $this->addClass('InputfieldToggleUseDeselect', 'wrapClass'); } return parent::renderReady($parent, $renderValueMode); } /** * Render value * * @return string * */ public function ___renderValue() { $label = $this->getValueLabel($this->attr('value')); $value = $this->formatLabel($label, true); return $value; } /** * Render input element(s) * * @return string * */ public function ___render() { $value = $this->val(); $default = $this->getSetting('defaultOption'); // check if we should assign a default value if($default && ("$value" === self::valueUnknown || !strlen("$value"))) { if($default === 'yes') { $this->val(self::valueYes); } else if($default === 'no') { $this->val(self::valueNo); } else if($default === 'other' && $this->useOther) { $this->val(self::valueOther); } } $f = $this->getInputfield(); if($f && $f !== $this) { $f->val($this->val()); $out = $f->render(); } else { $out = $this->renderToggle(); } // hidden input to indicate presence when no selection is made (like with radios) /** @var InputfieldButton $btn */ $button = $this->wire('modules')->get('InputfieldButton'); $button->setSecondary(true); $button->val('1'); $button->removeAttr('name'); $input = $this->wire('modules')->get('InputfieldText'); $input->attr('name', "_{$this->name}_"); $input->val(1); $out .= "
" . $input->render() . $button->render() . "
"; return $out; } /** * Render default input toggles * * @return string * */ protected function renderToggle() { $id = $this->attr('id'); $name = $this->attr('name'); $checkedValue = $this->val(); $out = ''; foreach($this->getOptions() as $value => $label) { $checked = "$checkedValue" === "$value" ? "checked " : ""; $inputClass = $checked ? 'InputfieldToggleChecked' : ''; $labelClass = $checked ? 'InputfieldToggleCurrent' : ''; $label = $this->formatLabel($label); $out .= "" . ""; } return "
" . "
$out
" . "
"; } /** * Process input * * @param WireInputData $input * @return $this * */ public function ___processInput(WireInputData $input) { $prevValue = $this->val(); $value = $input[$this->name]; $intValue = strlen($value) && ctype_digit("$value") ? (int) $value : null; if($value === null && $input["_{$this->name}_"] === null) { // input was not rendered in the submitted post, so should be ignored } else if($this->hasCustomOptions()) { // custom options if(isset($this->customOptions[$value])) $this->val($value); } else if($intValue === self::valueYes || $intValue === self::valueNo) { // yes or no selected $this->val($intValue); } else if($intValue === self::valueOther && $this->useOther) { // other selected $this->val($intValue); } else if($value === self::valueUnknown || $value === null) { // no selection $this->val(self::valueUnknown); } else { // something we don't recognize } if($this->val() !== $prevValue) { $this->trackChange('value', $prevValue, $this->val()); } return $this; } /** * Get labels for the given label type * * @param int $labelType Specify toggle type constant or omit to use configured toggle type. * @param Language|int|string|null Language or omit to use current user’s language. (default=null) * @return array Returned array has these indexes: * `no` (string): No/Off state label * `yes` (string): Yes/On state label * `other` (string): Other state label * `unknown` (string): No selection label * */ public function getLabels($labelType = null, $language = null) { if($labelType === null) $labelType = $this->labelType; /** @var Languages $langauges */ $languages = $this->wire('languages'); $setLanguage = false; $languageId = ''; $yes = ''; $no = ''; if($languages) { /** @var User $user */ $user = $this->wire('user'); if(empty($language)) { // use current user language $language = $user->language; } else if(is_int($language) || is_string($language)) { // get language from specified language ID or name $language = $languages->get($language); } if($language instanceof Page && $language->id != $user->language->id) { // use other specified language $languages->setLanguage($language); $setLanguage = true; } else { // use current user language $language = $user->language; } $languageId = $language && !$language->isDefault() ? $language->id : ''; } switch($labelType) { case self::labelTypeTrue: $yes = $this->_('True'); $no = $this->_('False'); break; case self::labelTypeOn: $yes = $this->_('On'); $no = $this->_('Off'); break; case self::labelTypeEnabled: $yes = $this->_('Enabled'); $no = $this->_('Disabled'); break; case self::labelTypeCustom: $yes = $languageId ? $this->get("yesLabel$languageId|yesLabel") : $this->yesLabel; $no = $languageId ? $this->get("noLabel$languageId|noLabel") : $this->noLabel; break; } // default (labelTypeYes) if(!strlen($yes)) $yes = $this->_('Yes'); if(!strlen($no)) $no = $this->_('No'); // other and unknown labels $other = $languageId ? $this->get("otherLabel$languageId|otherLabel") : $this->otherLabel; if(empty($other)) $other = $this->_('Other'); $unknown = $this->_('Unknown'); if($setLanguage && $languages) $languages->unsetLanguage(); return array( 'no' => $no, 'yes' => $yes, 'other' => $other, 'unknown' => $unknown ); } /** * Get all possible labels for all label types and all languages * * Returned array of labels (strings) indexed by "labelTypeNum:valueTypeName:languageName" * * @return array * */ public function getAllLabels() { if(!empty($this->allLabels)) return $this->allLabels; /** @var Languages|null $languages */ $languages = $this->wire('languages'); $all = array(); foreach($this->labelTypes as $labelType) { if($languages) { foreach($languages as $language) { foreach($this->getLabels($labelType, $language) as $valueType => $label) { $all["$labelType:$valueType:$language->name"] = $label; } } } else { foreach($this->getLabels($labelType) as $valueType => $label) { $all["$labelType:$valueType:default"] = $label; } } } return $all; } /** * Get the label for the currently set (or given) value * * @param bool|int|string|null $value Optionally provide value or omit to use currently set value attribute. * @param int|null $labelType Specify labelType constant or omit for selected label type. * @param Language|int|string $language * @return string Label string * */ public function getValueLabel($value = null, $labelType = null, $language = null) { if($value === null) $value = $this->val(); if($this->hasCustomOptions()) { // get custom defined option label from addOption() call (API usage only) return isset($this->customOptions[$value]) ? $this->customOptions[$value] : self::valueUnknown; } $labels = $this->getLabels($labelType, $language); if($value === null || $value === self::valueUnknown) return $labels['unknown']; if(is_bool($value)) return $value ? $labels['yes'] : $labels['no']; if($value === self::valueOther) return $labels['other']; if($value === self::valueYes) return $labels['yes']; if($value === self::valueNo) return $labels['no']; return $labels['unknown']; } /** * Format label for HTML output (entity encode, etc.) * * @param string $label * @param bool $allowIcon Allow icon markup to appear in label? * @return string * */ public function formatLabel($label, $allowIcon = true) { $label = $this->wire('sanitizer')->entities1($label); if(strpos($label, 'icon-') !== false && preg_match('/\bicon-([-_a-z0-9]+)/', $label, $matches)) { $name = $matches[1]; $icon = $allowIcon ? $icon = wireIconMarkup($name, 'fw') : ''; $label = str_replace("icon-$name", $icon, $label); } return trim($label); } /** * Get all selectable options as array of [ value => label ] * * @return array * */ public function getOptions() { // use custom options instead if any have been set from an addOption() call if($this->hasCustomOptions()) return $this->customOptions; // use built in toggle options $options = array(); $values = $this->useReverse ? array(self::valueNo, self::valueYes) : array(self::valueYes, self::valueNo); if($this->useOther) $values[] = self::valueOther; foreach($values as $value) { $options[$value] = $this->getValueLabel($value); } return $options; } /** * Add a selectable option (custom API usage only, overrides built-in options) * * Note that once you use this, your options take over and Toggle's default yes/no/other * are no longer applicable. This is for custom API use and is not used by FieldtypeToggle. * * @param int|string $value * @param null|string $label * @return $this * @throws WireException if you attempt to call this method when used with FieldtypeToggle * */ public function addOption($value, $label = null) { if($this->hasFieldtype) { throw new WireException('The addOption() method is not available for FieldtypeToggle'); } if($label === null) $label = $value; $this->customOptions[$value] = $label; return $this; } /** * Set all options with array of [ value => label ] (custom API usage only, overrides built-in options) * * Once you use this (with a non-empty array, your set options take over and the * built-in yes/no/other no longer apply. This is for custom API use and is not used * by FieldtypeToggle. * * The value for each option must be an integer value between -128 and 127. * * @param array $options * @return $this * @throws WireException if you attempt to call this method when used with FieldtypeToggle * */ public function setOptions(array $options) { $this->customOptions = array(); foreach($options as $key => $value) { $this->addOption($key, $value); } return $this; } /** * Are custom options in use? * * @return bool * */ protected function hasCustomOptions() { return count($this->customOptions) > 0; } /** * Return a list of config property names allowed for fieldgroup/template context * * @param Field $field * @return array of Inputfield names * @see Fieldtype::getConfigAllowContext() * */ public function ___getConfigAllowContext($field) { return array_merge(parent::___getConfigAllowContext($field), array( 'labelType', 'inputfieldClass', 'yesLabel', 'noLabel', 'otherLabel', 'useVertical', 'useDeselect', 'useOther', 'useReverse', 'defaultOption', )); } /** * Configure Inputfield * * @return InputfieldWrapper * */ public function ___getConfigInputfields() { /** @var Modules $modules */ $modules = $this->wire('modules'); /** @var Languages $languages */ $languages = $this->wire('languages'); $inputfields = parent::___getConfigInputfields(); if($this->hasFieldtype) { /** @var InputfieldFieldset $fieldset */ $fieldset = $modules->get('InputfieldFieldset'); $fieldset->label = $this->_('Toggle field labels and input settings'); $fieldset->icon = 'toggle-on'; $inputfields->prepend($fieldset); } else { $fieldset = $inputfields; } $removals = array('defaultValue'); foreach($removals as $name) { $f = $inputfields->getChildByName($name); if($f) $inputfields->remove($f); } /** @var InputfieldRadios $f */ $f = $modules->get('InputfieldRadios'); $f->attr('name', 'labelType'); $f->label = $this->_('Label type'); foreach($this->labelTypes as $labelType) { if($labelType == self::labelTypeCustom) { $label = $this->_('Custom'); } else { $label = $this->getValueLabel(self::valueYes, $labelType) . '/' . $this->getValueLabel(self::valueNo, $labelType); } $f->addOption($labelType, $label); } $f->attr('value', (int) $this->labelType); $f->columnWidth = 34; $fieldset->add($f); /** @var InputfieldRadios $f */ $f = $modules->get('InputfieldRadios'); $f->attr('name', 'inputfieldClass'); $f->label = $this->_('Input type'); $f->addOption('0', $this->_('Toggle buttons')); foreach($modules->findByPrefix('Inputfield') as $name) { if(!wireInstanceOf($name, 'InputfieldSelect')) continue; if(wireInstanceOf($name, 'InputfieldHasArrayValue')) continue; $label = str_replace('Inputfield', '', $name); $f->addOption($name, $label); } $f->val($this->getSetting('inputfieldClass')); $f->columnWidth = 33; $fieldset->add($f); /** @var InputfieldRadios $f */ $f = $modules->get('InputfieldRadios'); $f->attr('name', 'useVertical'); $f->label = $this->_('Radios'); $f->addOption(0, $this->_('Horizontal')); $f->addOption(1, $this->_('Vertical')); $f->val($this->useVertical ? 1 : 0); $f->columnWidth = 33; $f->showIf = 'inputfieldClass=InputfieldRadios'; $fieldset->add($f); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'useOther'); $f->label = $this->_('Use a 3rd “other” option?'); if($this->useOther) $f->attr('checked', 'checked'); $f->columnWidth = 34; $fieldset->add($f); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'useReverse'); $f->label = $this->_('Reverse order of yes/no options?'); if($this->useReverse) $f->attr('checked', 'checked'); $f->columnWidth = 33; $fieldset->add($f); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'useDeselect'); $f->label = $this->_('Support click to de-select?'); $f->showIf = "inputfieldClass=0|InputfieldRadios"; if($this->useDeselect) $f->attr('checked', 'checked'); $f->columnWidth = 33; $f->showIf = 'defaultOption=none'; $fieldset->add($f); $customStates = array( 'yesLabel' => $this->_('yes/on'), 'noLabel' => $this->_('no/off'), ); $labelFor = $this->_('Label for “%s” option'); /** @var InputfieldText $f */ foreach($customStates as $name => $label) { $f = $modules->get('InputfieldText'); $f->attr('name', $name); $f->label = sprintf($labelFor, $label); $f->showIf = 'labelType=' . self::labelTypeCustom; $f->val($this->get($name)); $f->columnWidth = 50; if($languages) { $f->useLanguages = true; foreach($languages as $language) { $langValue = $this->get("$name$language"); if(!$language->isDefault()) $f->set("value$language", $langValue); } } $fieldset->add($f); } /** @var InputfieldText $f */ $f = $modules->get('InputfieldText'); $f->attr('name', 'otherLabel'); $f->label = sprintf($labelFor, $this->_('other')); $f->showIf = 'useOther=1'; $f->val($this->get('otherLabel')); if($languages) { $f->useLanguages = true; foreach($languages as $language) { if(!$language->isDefault()) $f->set("value$language", $this->get("otherLabel$language")); } } $fieldset->add($f); /** @var InputfieldToggle $f */ $f = $modules->get('InputfieldToggle'); $f->set('inputfieldClass', $this->inputfieldClass); $f->attr('name', 'defaultOption'); $f->label = $this->_('Default selected option'); $f->addOption('yes', $this->getValueLabel(self::valueYes)); $f->addOption('no', $this->getValueLabel(self::valueNo)); if($this->useOther) $f->addOption('other', $this->getValueLabel(self::valueOther)); $f->addOption('none', $this->_('No selection')); $f->val($this->defaultOption); $f->addClass('InputfieldToggle', 'wrapClass'); $fieldset->add($f); return $inputfields; } }