From e94e10c6312a49497765eb39aa47bda4bd0fa955 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 30 Aug 2019 11:34:07 -0400 Subject: [PATCH] Add new "Toggle" Inputfield module providing an often more useful alternative to the InputfieldCheckbox module. There is also an accompanying FieldtypeToggle, but it is still in development so probably won't be till next week's commits. --- wire/core/Inputfield.php | 10 +- wire/core/InputfieldWrapper.php | 5 +- .../InputfieldPage/InputfieldPage.module | 6 +- .../InputfieldToggle/InputfieldToggle.module | 660 ++++++++++++++++++ 4 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module diff --git a/wire/core/Inputfield.php b/wire/core/Inputfield.php index 66916245..f47cc73c 100644 --- a/wire/core/Inputfield.php +++ b/wire/core/Inputfield.php @@ -214,7 +214,7 @@ abstract class Inputfield extends WireData implements Module { const skipLabelFor = true; /** - * Don't use a label header element at all (basically, skip the label) + * Don't show a visible header (likewise, do not show the label) * #pw-group-skipLabel-constants * */ @@ -227,6 +227,14 @@ abstract class Inputfield extends WireData implements Module { */ const skipLabelBlank = 4; + /** + * Do not render any markup for the header/label at all + * #pw-group-skipLabel-constants + * @since 3.0.139 + * + */ + const skipLabelMarkup = 8; + /** * Plain text: no type of markdown or HTML allowed * #pw-group-textFormat-constants diff --git a/wire/core/InputfieldWrapper.php b/wire/core/InputfieldWrapper.php index b79cde74..c9e07fbc 100644 --- a/wire/core/InputfieldWrapper.php +++ b/wire/core/InputfieldWrapper.php @@ -583,7 +583,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre if(!strlen($label) && $skipLabel !== Inputfield::skipLabelBlank && $inputfield->className() != 'InputfieldWrapper') { $label = $inputfield->attr('name'); } - if($label || $quietMode) { + if(($label || $quietMode) && $skipLabel !== Inputfield::skipLabelMarkup) { $for = $skipLabel || $quietMode ? '' : $inputfield->attr('id'); // if $inputfield has a property of entityEncodeLabel with a value of boolean FALSE, we don't entity encode $entityEncodeLabel = $inputfield->getSetting('entityEncodeLabel'); @@ -616,6 +616,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre } else if(strpos($label, '{class}') !== false) { $label = str_replace('{class}', '', $label); } + } else if($skipLabel === Inputfield::skipLabelMarkup) { + // no header and no markup for header + $label = ''; } else { // no header // $inputfield->addClass('InputfieldNoHeader', 'wrapClass'); diff --git a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module index 645f1d5b..8aca8578 100644 --- a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module +++ b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module @@ -13,7 +13,7 @@ * @property int $template_id * @property array $template_ids * @property int $parent_id - * @property string $inputfield + * @property string $inputfield Inputfield class used for input * @property string $labelFieldName Field name to use for label (note: this will be "." if $labelFieldFormat is in use). * @property string $labelFieldFormat Formatting string for $page->getMarkup() as alternative to $labelFieldName * @property string $findPagesCode @@ -22,6 +22,7 @@ * @property int|bool $addable * @property int|bool $allowUnpub * @property int $derefAsPage + * @property-read string $inputfieldClass Public property alias of protected getInputfieldClass() method * @property array $inputfieldClasses * * @method string renderAddable() @@ -311,12 +312,13 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { } public function getSetting($key) { + if($key === 'inputfieldClass') return $this->getInputfieldClass(); if($key === 'template_ids') return $this->getTemplateIDs(); $value = parent::getSetting($key); if($key === 'template_id' && empty($value)) { $templateIDs = $this->getTemplateIDs(); if(!empty($templateIDs)) $value = reset($templateIDs); - } + } return $value; } diff --git a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module new file mode 100644 index 00000000..711c352b --- /dev/null +++ b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module @@ -0,0 +1,660 @@ + __('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 = ''; + + // default or fallback Inputfield clasr + const defaultInputfieldClass = 'InputfieldRadios'; + + /** + * 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(); + + /** + * 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('defaultOption', 'none'); + $this->set('inputfieldClass', self::defaultInputfieldClass); + + $this->attr('value', self::valueUnknown); + + $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::__construct(); + } + + /** + * Is the current value empty? (i.e. no selection) + * + * @return bool + * + */ + public function isEmpty() { + $value = $this->val(); + if($value === '') return true; + if(is_int($value) && $value > -1) 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 + * @return int|string + * + */ + public function sanitizeValue($value) { + + if($value === null) return self::valueUnknown; + if(is_bool($value)) return $value ? self::valueYes : self::valueNo; + + $intValue = strlen("$value") && ctype_digit("$value") ? (int) $value : ''; + $strValue = strtolower("$value"); + + 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 + } + + 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 + * + */ + public function ___getInputfield() { + + if($this->inputfield) return $this->inputfield; + + $class = $this->getSetting('inputfieldClass'); + if(empty($class)) $class = self::defaultInputfieldClass; + + $f = $this->wire('modules')->get($class); + if(!$f) $f = $this->wire('modules')->get(self::defaultInputfieldClass); + + $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); + } + + $labels = $this->getLabels($this->labelType); + + if($this->useReverse) { + $f->addOption(self::valueNo, $labels['no']); + $f->addOption(self::valueYes, $labels['yes']); + } else { + $f->addOption(self::valueYes, $labels['yes']); + $f->addOption(self::valueNo, $labels['no']); + } + + if($this->useOther) { + $f->addOption(self::valueOther, $labels['other']); + } + + $f->val($this->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->renderReady($parent, $renderValueMode); + return parent::renderReady($parent, $renderValueMode); + } + + /** + * Render value + * + * @return string + * + */ + public function ___renderValue() { + $label = $this->getValueLabel($this->attr('value')); + $value = $this->wire('sanitizer')->entities1($label); + return $value; + } + + /** + * Render input element(s) + * + * @return string + * + */ + public function ___render() { + + $value = $this->val(); + + // check if we should assign a default value + $default = $this->getSetting('defaultOption'); + 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) return "Unable to load Inputfield"; + $f->val($this->val()); + + $out = $f->render(); + + $out .= "
" . print_r($this->getAllLabels(), true) . "
"; + + return $out; + } + + /** + * Process input + * + * @param WireInputData $input + * @return $this + * + */ + public function ___processInput(WireInputData $input) { + + $value = $input[$this->name]; + $intValue = strlen($value) && ctype_digit("$value") ? (int) $value : null; + + if($value === null) { + // selection not present in input + + } 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) { + // no selection (not reachable when using radios) + $this->val(self::valueUnknown); + + } else { + // something we don't recognize + } + + 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) { + + $labels = $this->getLabels($labelType, $language); + + if($value === null) $value = $this->attr('value'); + 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']; + } + + /** + * Get label used for yes/on option + * + * @param int|null $labelType Specify labelType constant or omit for selected label type. + * @param Language|int|string $language + * @return string + * + */ + public function getYesLabel($labelType = null, $language = null) { + return $this->getValueLabel(self::valueYes, $labelType, $language); + } + + /** + * Get label used for no/off option + * + * @param int|null $labelType Specify labelType constant or omit for selected label type. + * @param Language|int|string $language + * @return string + * + */ + public function getNoLabel($labelType = null, $language = null) { + return $this->getValueLabel(self::valueNo, $labelType, $language); + } + + /** + * Get label used for 3rd/other option + * + * @param int|null $labelType Specify labelType constant or omit for selected label type. + * @param Language|int|string $language + * @return string + * + */ + public function getOtherLabel($labelType = null, $language = null) { + return $this->getValueLabel(self::valueOther, $labelType, $language); + } + + /** + * Get label used for unknown/no-selection option + * + * @param int|null $labelType Specify labelType constant or omit for selected label type. + * @param Language|int|string $language + * @return string + * + */ + public function getUnknownLabel($labelType = null, $language = null) { + return $this->getValueLabel(self::valueUnknown, $labelType, $language); + } + + /** + * Configure Inputfield + * + * @return InputfieldWrapper + * + */ + public function ___getConfigInputfields() { + + /** @var Modules $modules */ + $modules = $this->wire('modules'); + + /** @var Languages $languages */ + $languages = $this->wire('languages'); + + $inputfields = parent::___getConfigInputfields(); + + $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->getYesLabel($labelType) . '/' . $this->getNoLabel($labelType); + } + $f->addOption($labelType, $label); + } + $f->attr('value', (int) $this->labelType); + $f->columnWidth = 34; + $inputfields->add($f); + + /** @var InputfieldRadios $f */ + $f = $modules->get('InputfieldRadios'); + $f->attr('name', 'inputfieldClass'); + $f->label = $this->_('Input type'); + foreach($modules->findByPrefix('Inputfield') as $name) { + if(!wireInstanceOf($name, 'InputfieldSelect')) continue; + if(wireInstanceOf($name, 'InputfieldHasArrayValue')) continue; + $f->addOption($name, str_replace('Inputfield', '', $name)); + } + $f->val($this->getSetting('inputfieldClass')); + $f->columnWidth = 33; + $inputfields->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'; + $inputfields->add($f); + + $customStates = array( + 'yesLabel' => $this->_('Yes/On'), + 'noLabel' => $this->_('No/Off'), + ); + + /** @var InputfieldText $f */ + foreach($customStates as $name => $label) { + $f = $modules->get('InputfieldText'); + $f->attr('name', $name); + $f->label = sprintf($this->_('Label for “%s” option'), $label); + $f->showIf = 'labelType=' . self::labelTypeCustom; + $f->attr('value', $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); + } + } + $inputfields->add($f); + } + + /** @var InputfieldText $f */ + $f = $modules->get('InputfieldText'); + $f->attr('name', 'otherLabel'); + $f->label = sprintf($this->_('Label for 3rd option')); + $f->showIf = 'useOther=1'; + $f->attr('value', $this->get('otherLabel')); + $f->columnWidth = 50; + if($languages) { + $f->useLanguages = true; + foreach($languages as $language) { + if(!$language->isDefault()) $f->set("value$language", $this->get("otherLabel$language")); + } + } + $inputfields->add($f); + + /** @var InputfieldCheckbox $f */ + $f = $modules->get('InputfieldCheckbox'); + $f->attr('name', 'useOther'); + $f->label = $this->_('Show a 3rd option?'); + if($this->useOther) $f->attr('checked', 'checked'); + $f->columnWidth = 50; + $inputfields->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'); + $inputfields->add($f); + + /** @var InputfieldRadios $f */ + $f = $modules->get('InputfieldRadios'); + $f->attr('name', 'defaultOption'); + $f->label = $this->_('Default selected option'); + $f->addOption('yes', $this->getYesLabel()); + $f->addOption('no', $this->getNoLabel()); + if($this->useOther) $f->addOption('other', $this->getOtherLabel()); + $f->addOption('none', $this->_('No selection')); + $f->optionColumns = 1; + $f->attr('value', $this->defaultOption); + $inputfields->add($f); + + return $inputfields; + } +}