diff --git a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.css b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.css new file mode 100644 index 00000000..5da8e41c --- /dev/null +++ b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.css @@ -0,0 +1,43 @@ +.InputfieldToggleGroup { + display: flex; + float: left; +} + +.InputfieldToggleGroup input { + position: absolute !important; + clip: rect(0, 0, 0, 0); + height: 1px; + width: 1px; + border: 0; + overflow: hidden; +} + +.InputfieldToggleGroup label { + background-color: #eee; + color: rgba(0, 0, 0, 0.6); + line-height: 1; + text-align: center; + padding: 8px 16px; + margin-right: -1px; + border: 1px solid rgba(0,0,0,0.1); + font-size: 16px; +} + +.InputfieldToggleGroup label:hover { + cursor: pointer; +} + +.InputfieldToggleGroup input:checked + label { + background-color: #999; + color: #fff; + box-shadow: none; +} + +.InputfieldToggleGroup label:first-of-type { + border-radius: 4px 0 0 4px; +} + +.InputfieldToggleGroup label:last-of-type { + border-radius: 0 4px 4px 0; +} + diff --git a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.js b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.js new file mode 100644 index 00000000..b43612a7 --- /dev/null +++ b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.js @@ -0,0 +1,85 @@ +function InputfieldToggleInit() { + + // this becomes true when we are in a click event, used to avoid double calls to handler + var isClick = false; + + // event handler for labels/inputs in a .InputfieldToggleUseDeselect container + function toggleInputEvent($input) { + + var cls = 'InputfieldToggleChecked'; + + // allow for labels as prev sibling of input or label as parent element of input + // var $label = $input.prev('label').length ? $input.prev('label') : $input.closest('label'); + var $prevInput = $input.closest('.Inputfield').find('input.' + cls); + // var $prevLabel = $prevInput.prev('label').length ? $prevInput.prev('label') : $prevInput.closest('label'); + + // check of another item was clicked when an existing selection was in place + if($prevInput.length && $prevInput.attr('id') != $input.attr('id')) { + // remove our custom class from existing selection + $prevInput.removeClass(cls).removeAttr('checked'); + } + + // check if clicked input was already checked + if($input.hasClass(cls) && $input.closest('.InputfieldToggleUseDeselect').length) { + // if clicked input was already checked, now make it un-checked + $input.removeAttr('checked').removeClass(cls); + // if this de-select was the first selection in the request, it's necessary to remove + // the checked attribute again a short while later for some reason + setTimeout(function() { $input.removeAttr('checked') }, 100); + } else { + // input was just checked (and wasn't before), so add our checked class to the input + // $input.attr('checked', 'checked').addClass(cls); + $input.addClass(cls); + } + } + + $(document).on('change', '.InputfieldToggle input', function() { + // change event for de-selectable radios + if(isClick) return false; + toggleInputEvent($(this)); + + }).on('click', '.InputfieldToggle label:not(.InputfieldHeader)', function() { + // click event for de-selectable radios + if(isClick) return false; + var $label = $(this); + var $input = $label.prev('input.InputfieldToggleChecked'); // toggle buttons + if(!$input.length) $input = $label.children('input.InputfieldToggleChecked'); // radios + if(!$input.length) return; + isClick = true; + toggleInputEvent($input); + setTimeout(function() { isClick = false; }, 200); + }); + + // button style for default toggle button group + // inherit colors from existing inputs and buttons + var $button = $('button.ui-button:eq(0)'); + var $input = $('.InputfieldForm input[type=text]:eq(0)'); + if($button.length && $input.length) { + var onBgcolor, onColor, offBgcolor, offColor, borderColor, style; + onBgcolor = $button.css('background-color'); + onColor = $button.css('color'); + offBgcolor = $input.css('background-color'); + offColor = $input.css('color'); + borderColor = $input.css('border-color'); + style = + ""; + + $('body').append(style); + } +} + + +jQuery(document).ready(function($) { + InputfieldToggleInit(); +}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.min.js b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.min.js new file mode 100644 index 00000000..c8712430 --- /dev/null +++ b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.min.js @@ -0,0 +1 @@ +function InputfieldToggleInit(){var isClick=false;function toggleInputEvent($input){var cls="InputfieldToggleChecked";var $prevInput=$input.closest(".Inputfield").find("input."+cls);if($prevInput.length&&$prevInput.attr("id")!=$input.attr("id")){$prevInput.removeClass(cls).removeAttr("checked")}if($input.hasClass(cls)&&$input.closest(".InputfieldToggleUseDeselect").length){$input.removeAttr("checked").removeClass(cls);setTimeout(function(){$input.removeAttr("checked")},100)}else{$input.addClass(cls)}}$(document).on("change",".InputfieldToggle input",function(){if(isClick)return false;toggleInputEvent($(this))}).on("click",".InputfieldToggle label:not(.InputfieldHeader)",function(){if(isClick)return false;var $label=$(this);var $input=$label.prev("input.InputfieldToggleChecked");if(!$input.length)$input=$label.children("input.InputfieldToggleChecked");if(!$input.length)return;isClick=true;toggleInputEvent($input);setTimeout(function(){isClick=false},200)});var $button=$("button.ui-button:eq(0)");var $input=$(".InputfieldForm input[type=text]:eq(0)");if($button.length&&$input.length){var onBgcolor,onColor,offBgcolor,offColor,borderColor,style;onBgcolor=$button.css("background-color");onColor=$button.css("color");offBgcolor=$input.css("background-color");offColor=$input.css("color");borderColor=$input.css("border-color");style="";$("body").append(style)}}jQuery(document).ready(function($){InputfieldToggleInit()}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module index a5587eaa..c89080fa 100644 --- a/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module +++ b/wire/modules/Inputfield/InputfieldToggle/InputfieldToggle.module @@ -9,17 +9,17 @@ * ProcessWire 3.x, Copyright 2019 by Ryan Cramer * https://processwire.com * - * @property int $labelType - * @property int $valueType + * @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 - * @property int|bool $useReverse - * @property int|bool $useOther - * @property bool|int $useVertical - * @property int $defaultValue - * @property int|string $defaultOption - * @property string $inputfieldClass + * @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? (default=false) + * @property bool|int $useDeselect Allow radios or toggles to be de-selected, enabling possibility of no-selection? (default=false) + * @property int|string $defaultOption Default selected value of 0, 1, 2 or '' (default='') + * @property string $inputfieldClass Inputfield class to use or blank for this toggle buttons (default='') * * @method InputfieldSelect|InputfieldRadios getInputfield() * @@ -47,9 +47,6 @@ class InputfieldToggle extends Inputfield { const valueOther = 2; const valueUnknown = ''; - // default or fallback Inputfield clasr - const defaultInputfieldClass = 'InputfieldRadios'; - /** * Array of all label types * @@ -93,6 +90,14 @@ class InputfieldToggle extends Inputfield { */ protected $allLabels = array(); + /** + * Manually added options of [ value => label ] + * + * @var array + * + */ + protected $options = array(); + /** * Construct and set default settings * @@ -106,8 +111,9 @@ class InputfieldToggle extends Inputfield { $this->set('useOther', 0); $this->set('useReverse', 0); $this->set('useVertical', 0); + $this->set('useDeselect', 0); $this->set('defaultOption', 'none'); - $this->set('inputfieldClass', self::defaultInputfieldClass); + $this->set('inputfieldClass', '0'); $this->attr('value', self::valueUnknown); @@ -132,8 +138,14 @@ class InputfieldToggle extends Inputfield { */ public function isEmpty() { $value = $this->val(); - if($value === '') return true; - if(is_int($value) && $value > -1) return false; + if($value === self::valueUnknown) return true; + if(is_int($value)) { + if($this->hasOptions()) { + if(isset($this->options[$value])) return false; + } else { + if($value > -1) return false; + } + } if($value === self::valueOther && $this->useOther) return false; return true; } @@ -153,7 +165,11 @@ class InputfieldToggle extends Inputfield { $intValue = strlen("$value") && ctype_digit("$value") ? (int) $value : ''; $strValue = strtolower("$value"); - if($intValue === self::valueNo || $intValue === self::valueYes) { + if($this->hasOptions()) { + if($intValue !== '') $value = $intValue; + $value = isset($this->options[$value]) ? $value : self::valueUnknown; + + } else if($intValue === self::valueNo || $intValue === self::valueYes) { $value = $intValue; } else if($intValue === self::valueOther) { @@ -203,18 +219,26 @@ class InputfieldToggle extends Inputfield { /** * Get the delegated Inputfield that will be used for rendering selectable options * - * @return InputfieldRadios|InputfieldSelect + * @return InputfieldRadios|InputfieldSelect|InputfieldToggle * */ public function ___getInputfield() { - + if($this->inputfield) return $this->inputfield; $class = $this->getSetting('inputfieldClass'); - if(empty($class)) $class = self::defaultInputfieldClass; + if(empty($class) || $class === $this->className()) { + if($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->wire('modules')->get(self::defaultInputfieldClass); + if(!$f || $f === $this) return $this; $this->addClass($class, 'wrapClass'); @@ -226,22 +250,17 @@ class InputfieldToggle extends Inputfield { 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']); + $val = $this->val(); + $options = $this->getOptions(); + $f->addOptions($options); + + if(isset($options[$val]) && method_exists($f, 'addOptionAttributes')) { + $f->addOptionAttributes($val, array('input.class' => 'InputfieldToggleChecked')); } - if($this->useOther) { - $f->addOption(self::valueOther, $labels['other']); - } + $f->val($val); - $f->val($this->val()); $this->inputfield = $f; return $f; @@ -257,7 +276,10 @@ class InputfieldToggle extends Inputfield { */ public function renderReady(Inputfield $parent = null, $renderValueMode = false) { $f = $this->getInputfield(); - if($f) $f->renderReady($parent, $renderValueMode); + if($f && $f !== $this) $f->renderReady($parent, $renderValueMode); + if($this->useDeselect && $this->defaultOption === 'none') { + $this->addClass('InputfieldToggleUseDeselect', 'wrapClass'); + } return parent::renderReady($parent, $renderValueMode); } @@ -269,7 +291,7 @@ class InputfieldToggle extends Inputfield { */ public function ___renderValue() { $label = $this->getValueLabel($this->attr('value')); - $value = $this->wire('sanitizer')->entities1($label); + $value = $this->formatLabel($label, true); return $value; } @@ -282,9 +304,9 @@ class InputfieldToggle extends Inputfield { public function ___render() { $value = $this->val(); - - // check if we should assign a default value $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); @@ -296,10 +318,46 @@ class InputfieldToggle extends Inputfield { } $f = $this->getInputfield(); - if(!$f) return "Unable to load Inputfield"; - $f->val($this->val()); + + 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) + $out .= ""; - return $f->render(); + return $out; + } + + /** + * Render default input toggles + * + * @return string + * + */ + protected function renderToggle() { + + $id = $this->attr('id'); + $name = $this->attr('name'); + $checkedValue = $this->val(); + $out = ''; + + foreach($this->getOptions() as $value => $label) { + $checked = "$checkedValue" === "$value" ? "checked " : ""; + $class = $checked ? 'InputfieldToggleChecked' : ''; + $label = $this->formatLabel($label); + $out .= + "" . + ""; + } + + return + "