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 constantsconst labelTypeYes = 0;const labelTypeTrue = 1;const labelTypeOn = 2;const labelTypeEnabled = 3;const labelTypeCustom = 100;// value constantsconst 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 valueif($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 optionsif(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() callif($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;}}