artabro/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module
2024-08-27 11:35:37 +02:00

889 lines
25 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 users 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;
}
}