From 85f87cf2a751743e59ed46943130835fced2cac2 Mon Sep 17 00:00:00 2001 From: Jared Hancock <jared@osticket.com> Date: Fri, 12 Sep 2014 20:08:03 -0500 Subject: [PATCH] forms: Add new visibility property This allows fields to specify a visibility constraint that will be evaluated in real time in the browser to automatically show and hide fields that should be hidden based on values of other fields in the same form. Validation is not performed on fields server-side if they are considered invisible when submitted. --- include/class.forms.php | 185 +++++++++++++++++- .../templates/dynamic-field-config.tmpl.php | 15 +- scp/css/scp.css | 3 + 3 files changed, 192 insertions(+), 11 deletions(-) diff --git a/include/class.forms.php b/include/class.forms.php index 585772fdf..6552d5c13 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -236,7 +236,9 @@ class FormField { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) ? $this->value : $this->parse($this->getWidget()->value); - $this->validateEntry($this->_clean); + + if ($this->isVisible()) + $this->validateEntry($this->_clean); } return $this->_clean; } @@ -291,6 +293,21 @@ class FormField { } } + /** + * isVisible + * + * If this field has visibility configuration, then it will parse the + * constraints with the visibility configuration to determine if the + * field is visible and should be considered for validation + */ + function isVisible() { + $config = $this->getConfiguration(); + if ($this->get('visibility') instanceof VisibilityConstraint) { + return $this->get('visibility')->isVisible($this); + } + return true; + } + /** * parse * @@ -452,7 +469,11 @@ class FormField { } function render($mode=null) { - return $this->getWidget()->render($mode); + $rv = $this->getWidget()->render($mode); + if ($v = $this->get('visibility')) { + $v->emitJavascript($this); + } + return $rv; } function renderExtras($mode=null) { @@ -1663,6 +1684,7 @@ class TextareaWidget extends Widget { <span style="display:inline-block;width:100%"> <textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class .' placeholder="'.$config['placeholder'].'"'; ?> + id="<?php echo $this->name; ?>" name="<?php echo $this->name; ?>"><?php echo Format::htmlchars($this->value); ?></textarea> @@ -1676,7 +1698,7 @@ class PhoneNumberWidget extends Widget { $config = $this->field->getConfiguration(); list($phone, $ext) = explode("X", $this->value); ?> - <input type="text" name="<?php echo $this->name; ?>" value="<?php + <input id="<?php echo $this->name; ?>" type="text" name="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($phone); ?>"/><?php // Allow display of extension field even if disabled if the phone // number being edited has an extension @@ -1806,7 +1828,8 @@ class CheckboxWidget extends Widget { if (!isset($this->value)) $this->value = $this->field->get('default'); ?> - <input style="vertical-align:top;" type="checkbox" name="<?php echo $this->name; ?>[]" <?php + <input id="<?php echo $this->name; ?>" style="vertical-align:top;" + type="checkbox" name="<?php echo $this->name; ?>[]" <?php if ($this->value) echo 'checked="checked"'; ?> value="<?php echo $this->field->get('id'); ?>"/> <?php @@ -1841,6 +1864,7 @@ class DatetimePickerWidget extends Widget { } ?> <input type="text" name="<?php echo $this->name; ?>" + id="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($this->value); ?>" size="12" autocomplete="off" class="dp" /> <script type="text/javascript"> @@ -2035,4 +2059,157 @@ class FileUploadWidget extends Widget { class FileUploadError extends Exception {} +class VisibilityConstraint { + + const HIDDEN = 0x0001; + const VISIBLE = 0x0002; + + var $initial; + var $constraint; + + function __construct($constraint, $initial=self::VISIBLE) { + $this->constraint = $constraint; + $this->initial = $initial; + } + + function emitJavascript($field) { + $func = 'recheck'; + $form = $field->getForm(); +?> + <script type="text/javascript"> + !(function() { + var <?php echo $func; ?> = function() { + var target = $('#field-<?php echo $field->getWidget()->name; ?>'); + +<?php $fields = $this->getAllFields($this->constraint); + foreach ($fields as $f) { + $field = $form->getField($f); + echo sprintf('var %1$s = $("#%1$s");', + $field->getWidget()->name); + } + $expression = $this->compileQ($this->constraint, $form); +?> + target.slideToggle(<?php echo $expression; ?>); + }; + +<?php foreach ($fields as $f) { + $w = $form->getField($f)->getWidget(); +?> + $('#<?php echo $w->name; ?>').on('change', <?php echo $func; ?>); +<?php } ?> + })(); + </script><?php + } + + /** + * Determines if the field was visible when the form was submitted + */ + function isVisible($field) { + return $this->compileQPhp($this->constraint, $field); + } + + function compileQPhp(Q $Q, $field) { + $form = $field->getForm(); + $expr = array(); + foreach ($Q->constraints as $c=>$value) { + if ($value instanceof Q) { + $expr[] = $this->compileQPhp($value, $field); + } + else { + @list($f, $op) = explode('__', $c, 2); + $field = $form->getField($f); + $wval = $field->getClean(); + switch ($op) { + case 'eq': + case null: + $expr[] = $wval == $value; + } + } + } + $glue = $Q->isOred() + ? function($a, $b) { return $a || $b; } + : function($a, $b) { return $a && $b; }; + $initial = !$Q->isOred(); + $expression = array_reduce($expr, $glue, $initial); + if ($Q->isNegated) + $expression = !$expression; + return $expression; + } + + function getAllFields(Q $Q, &$fields=array()) { + foreach ($Q->constraints as $c=>$value) { + if ($c instanceof Q) { + $this->getAllFields($c, $fields); + } + else { + list($f, $op) = explode('__', $c, 2); + $fields[$f] = true; + } + } + return array_keys($fields); + } + + function compileQ($Q, $form) { + $expr = array(); + foreach ($Q->constraints as $c=>$value) { + if ($value instanceof Q) { + $expr[] = $this->compileQ($value, $form); + } + else { + list($f, $op) = explode('__', $c, 2); + $name = $form->getField($f)->getWidget()->name; + switch ($op) { + case 'eq': + default: + $expr[] = sprintf('%s.val() == %s', $name, JsonDataEncoder::encode($value)); + } + } + } + $glue = $Q->isOred() ? ' || ' : ' && '; + $expression = implode($glue, $expr); + if (count($expr) > 1) + $expression = '('.$expression.')'; + if ($Q->isNegated) + $expression = '!'.$expression; + return $expression; + } +} + +class Q { + const NEGATED = 0x0001; + const ANY = 0x0002; + + var $constraints; + var $flags; + var $negated = false; + var $ored = false; + + function __construct($filter, $flags=0) { + $this->constraints = $filter; + $this->negated = $flags & self::NEGATED; + $this->ored = $flags & self::ANY; + } + + function isNegated() { + return $this->negated; + } + + function isOred() { + return $this->ored; + } + + function negate() { + $this->negated = !$this->negated; + return $this; + } + + static function not(array $constraints) { + return new static($constraints, self::NEGATED); + } + + static function any(array $constraints) { + return new static($constraints, self::ORED); + } +} + ?> diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php index af2574652..104c14e64 100644 --- a/include/staff/templates/dynamic-field-config.tmpl.php +++ b/include/staff/templates/dynamic-field-config.tmpl.php @@ -8,10 +8,15 @@ $form = $field->getConfigurationForm(); echo $form->getMedia(); foreach ($form->getFields() as $name=>$f) { ?> - <div class="flush-left custom-field"> - <div class="field-label"> + <div class="flush-left custom-field" id="field-<?php echo $f->getWidget()->name; + ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>> + <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>"> <label for="<?php echo $f->getWidget()->name; ?>"> - <?php echo Format::htmlchars($f->get('label')); ?>:</label> + <?php echo Format::htmlchars($f->get('label')); ?>: + <?php if ($f->get('required')) { ?> + <span class="error">*</span> + <?php } ?> + </label> <?php if ($f->get('hint')) { ?> <br/><em style="color:gray;display:inline-block"><?php @@ -21,10 +26,6 @@ </div><div> <?php $f->render(); - if ($f->get('required')) { ?> - <font class="error">*</font> - <?php - } ?> </div> <?php diff --git a/scp/css/scp.css b/scp/css/scp.css index 9389be034..5e3579d5e 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1948,3 +1948,6 @@ table.custom-info td { direction: ltr; unicode-bidi: embed; } +.required { + font-weight: bold; +} -- GitLab