Subversion Repositories web.active

Rev

Rev 1 | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * Toggle Inputfield
 * 
 * An Inputfield for handling an on/off toggle that maintains a boolean value (or null when no selection).
 * This provides an alternative to a single checkbox field. 
 * 
 * ProcessWire 3.x, Copyright 2020 by Ryan Cramer
 * https://processwire.com
 * 
 * API usage example
 * ~~~~~~
 * // Default behavior displays toggle of "Yes" and "No":
 * $f = $modules->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 .= "<div class='InputfieldToggleHelper'>" . $input->render() . $button->render() . "</div>";

    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 .= 
        "<input type='radio' id='{$id}_$value' name='$name' class='$inputClass' value='$value' $checked/>" . 
                "<label for='{$id}_$value' class='$labelClass'><span class='pw-no-select'>$label</span></label>";
    }
  
    return
      "<div class='pw-clearfix ui-helper-clearfix'>" . 
        "<div class='InputfieldToggleGroup'>$out</div>" . 
      "</div>";
  }
  
  /**
   * 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;
  }
}