Add form field dependency implementation (WIP)
authorMatthias Schmidt <gravatronics@live.com>
Sat, 13 Jan 2018 07:05:22 +0000 (08:05 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Sat, 13 Jan 2018 07:05:22 +0000 (08:05 +0100)
See #2509

26 files changed:
wcfsetup/install/files/acp/templates/__booleanFormField.tpl
wcfsetup/install/files/acp/templates/__formContainer.tpl
wcfsetup/install/files/acp/templates/__formContainerDependencies.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__formFieldFooter.tpl
wcfsetup/install/files/acp/templates/__formFieldHeader.tpl
wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__tabFormContainer.tpl
wcfsetup/install/files/acp/templates/__tabMenuFormContainer.tpl
wcfsetup/install/files/acp/templates/__tabTabMenuFormContainer.tpl
wcfsetup/install/files/acp/templates/__valueFormFieldDependency.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.js [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/DevtoolsFormBuilderTestForm.class.php
wcfsetup/install/files/lib/system/form/builder/IFormElement.class.php
wcfsetup/install/files/lib/system/form/builder/IFormNode.class.php
wcfsetup/install/files/lib/system/form/builder/TFormElement.class.php
wcfsetup/install/files/lib/system/form/builder/TFormNode.class.php
wcfsetup/install/files/lib/system/form/builder/TFormParentNode.class.php
wcfsetup/install/files/lib/system/form/builder/field/data/DefaultFormFieldDataProcessor.class.php
wcfsetup/install/files/lib/system/form/builder/field/dependency/AbstractFormFieldDependency.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/dependency/IFormFieldDependency.class.php
wcfsetup/install/files/lib/system/form/builder/field/dependency/NonEmptyFormFieldDependency.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/dependency/ValueFormFieldDependency.class.php [new file with mode: 0644]

index 625b02ed1d0eaab730756669cf40598bfcb7fca7..da10fb6550f508c2b63cc053d3032752ab8e3668 100644 (file)
@@ -2,7 +2,7 @@
 
 <ol class="flexibleButtonGroup">
        <li>
-               <input type="radio" id="{@$field->getPrefixedId()}" name="{@$field->getPrefixedId()}" value="1"{if $field->isAutofocused()} autofocus{/if}{if $field->isRequired()} required{/if}{if $field->isImmutable()} disabled{/if}{if $field->getValue()} checked{/if}>
+               <input type="radio" id="{@$field->getPrefixedId()}" name="{@$field->getPrefixedId()}" value="1" data-no-input-id="{@$field->getPrefixedId()}_no"{if $field->isAutofocused()} autofocus{/if}{if $field->isRequired()} required{/if}{if $field->isImmutable()} disabled{/if}{if $field->getValue()} checked{/if}>
                <label for="{@$field->getPrefixedId()}" class="green"><span class="icon icon16 fa-check"></span> {lang}wcf.global.form.boolean.yes{/lang}</label>
        </li>
        <li>
index 9bf9dd92170afb1d33f30deaaefa600b65a8483a..9be5eb52e07cdc04b15ec86a41525edfab0aad74 100644 (file)
@@ -1,4 +1,4 @@
-<section id="{@$container->getPrefixedId()}" class="section{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
+<section id="{@$container->getPrefixedId()}Container" class="section{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}>
        {if $container->getLabel() !== null}
                {if $container->getDescription() !== null}
                        <header class="sectionHeader">
@@ -12,3 +12,5 @@
        
        {include file='__formContainerChildren'}
 </section>
+
+{include file='__formContainerDependencies'}
diff --git a/wcfsetup/install/files/acp/templates/__formContainerDependencies.tpl b/wcfsetup/install/files/acp/templates/__formContainerDependencies.tpl
new file mode 100644 (file)
index 0000000..e9daa97
--- /dev/null
@@ -0,0 +1,7 @@
+{if !$container->getDependencies()|empty}
+       <script data-relocate="true">
+               {foreach from=$field->getDependencies() item=dependency}
+                       {@$dependency->getHtml()}
+               {/foreach}
+       </script>
+{/if}
diff --git a/wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl b/wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl
new file mode 100644 (file)
index 0000000..c3525c3
--- /dev/null
@@ -0,0 +1,7 @@
+{if !$field->getDependencies()|empty}
+       <script data-relocate="true">
+               {foreach from=$field->getDependencies() item=dependency}
+                       {@$dependency->getHtml()}
+               {/foreach}
+       </script>
+{/if}
index 3c3d9df472ae623d05ac0f3f5074d299d323bd90..bb5c6873902cd15ecb13f9d9448adbdbc28e9bb1 100644 (file)
@@ -1,4 +1,5 @@
                {include file='__formFieldDescription'}
                {include file='__formFieldErrors'}
+               {include file='__formFieldDependencies'}
        </dd>
 </dl>
index f0f29615cbe2a7e390cc6d11cd44016f0ead5c72..cc3438f97c8d39b43e6724a06805e73509e89bfe 100644 (file)
@@ -1,3 +1,3 @@
-<dl id="{@$field->getPrefixedId()}Container" {if !$field->getClasses()|empty} class="{implode from=$field->getClasses() item='class'}{$class}{/implode}"{/if}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
+<dl id="{@$field->getPrefixedId()}Container" {if !$field->getClasses()|empty} class="{implode from=$field->getClasses() item='class'}{$class}{/implode}"{/if}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$field->checkDependencies()} style="display: none;"{/if}>
        <dt><label for="{@$field->getPrefixedId()}">{@$field->getLabel()}</label></dt>
        <dd>
diff --git a/wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl b/wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl
new file mode 100644 (file)
index 0000000..1d119dd
--- /dev/null
@@ -0,0 +1,7 @@
+require(['WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty'], function(NonEmptyFieldDependency) {
+       // dependency '{@$dependency->getId()}'
+       new NonEmptyFieldDependency(
+               '{@$dependency->getDependentNode()->getPrefixedId()}Container',
+               '{@$dependency->getField()->getPrefixedId()}'
+       );
+});
index a1e6392d510d5921c1cb54bb47444615a5eb5035..c6e3c62d7d5ca51122c7775e3af185ec732db970 100644 (file)
@@ -1,3 +1,5 @@
-<div id="{@$container->getPrefixedId()}" class="tabMenuContent{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
+<div id="{@$container->getPrefixedId()}Container" class="tabMenuContent{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}>
        {include file='__formContainerChildren'}
 </div>
+
+{include file='__formContainerDependencies'}
index 77e79396a2737cf6111e2ec0aa6c64cf71a84465..e5f1176b53f241676e78c5a07574cd70d7e75318 100644 (file)
@@ -1,11 +1,14 @@
-<div id="{@$container->getPrefixedId()}" class="section tabMenuContainer{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
+<div id="{@$container->getPrefixedId()}Container" class="section tabMenuContainer{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}{if !$container->checkDependencies()} style="display: none;"{/if}>
        <nav class="tabMenu">
                <ul>
                        {foreach from=$container item='child'}
-                               <li><a href="{@$__wcf->getAnchor($child->getPrefixedId())}">{@$child->getLabel()}</a></li>
+                               {assign var='__tabMenuFormContainerChildId' value=$child->getPrefixedId()|concat:'Container'}
+                               <li><a href="{@$__wcf->getAnchor($__tabMenuFormContainerChildId)}">{@$child->getLabel()}</a></li>
                        {/foreach}
                </ul>
        </nav>
        
        {include file='__formContainerChildren'}
 </div>
+
+{include file='__formContainerDependencies'}
index 3c3362edb848df9242a842f376e0624f899b8db9..3f84fca1ec1edf6dae166339d70993c4562f3138 100644 (file)
@@ -1,4 +1,4 @@
-<div id="{@$container->getPrefixedId()}" class="tabMenuContainer tabMenuContent{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
+<div id="{@$container->getPrefixedId()}Container" class="tabMenuContainer tabMenuContent{foreach from=$container->getClasses() item='class'} {$class}{/foreach}"{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
        <nav class="menu">
                <ul>
                        {foreach from=$container item='child'}
@@ -9,3 +9,5 @@
        
        {include file='__formContainerChildren'}
 </div>
+
+{include file='__formContainerDependencies'}
diff --git a/wcfsetup/install/files/acp/templates/__valueFormFieldDependency.tpl b/wcfsetup/install/files/acp/templates/__valueFormFieldDependency.tpl
new file mode 100644 (file)
index 0000000..695c6d3
--- /dev/null
@@ -0,0 +1,7 @@
+require(['WoltLabSuite/Core/Form/Builder/Field/Dependency/Value'], function(ValueFieldDependency) {
+       // dependency '{@$dependency->getId()}'
+       new ValueFieldDependency(
+               '{@$dependency->getDependentNode()->getPrefixedId()}Container',
+               '{@$dependency->getField()->getPrefixedId()}'
+       ).values([ {implode from=$dependency->getValues() item=dependencyValue}'{$dependencyValue|encodeJS}'{/implode} ]);
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract.js
new file mode 100644 (file)
index 0000000..ad00c5c
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Abstract implementation of a form field dependency.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract
+ * @since      3.2
+ */
+define(['./Manager'], function(DependencyManager) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function Abstract(dependentElementId, fieldId) {
+               this.init(dependentElementId, fieldId);
+       };
+       Abstract.prototype = {
+               /**
+                * Checks if the dependency is met.
+                * 
+                * @abstract
+                */
+               checkDependency: function() {
+                       throw new Error("Implement me!")
+               },
+               
+               /**
+                * Return the node whose availability depends on the value of a field.
+                * 
+                * @return      {HtmlElement}   dependent node
+                */
+               getDependentNode: function() {
+                       return this._dependentElement;
+               },
+               
+               /**
+                * Returns the field the availability of the element dependents on.
+                * 
+                * @return      {HtmlElement}   field controlling element availability
+                */
+               getField: function() {
+                       return this._field;
+               },
+               
+               /**
+                * Returns all fields requiring `change` event listeners for this
+                * dependency to be properly resolved.
+                * 
+                * @return      {HtmlElement[]}         fields to register event listeners on
+                */
+               getFields: function() {
+                       return this._fields;
+               },
+               
+               /**
+                * Initializes the new dependency object.
+                * 
+                * @param       {string}        dependentElementId      id of the (container of the) dependent element
+                * @param       {string}        fieldId                 id of the field controlling element availability
+                * 
+                * @throws      {Error}                                 if either depenent element id or field id are invalid
+                */
+               init: function(dependentElementId, fieldId) {
+                       this._dependentElement = elById(dependentElementId);
+                       if (this._dependentElement === null) {
+                               throw new Error("Unknown dependent element with container id '" + dependentElementId + "Container'.");
+                       }
+                       
+                       this._field = elById(fieldId);
+                       if (this._field === null) {
+                               throw new Error("Unknown field with id '" + fieldId + "'.");
+                       }
+                       
+                       this._fields = [this._field];
+                       
+                       // handle special case of boolean form fields that have to form fields
+                       if (this._field.tagName === 'INPUT' && this._field.type === 'radio' && elData(this._field, 'no-input-id') !== '') {
+                               this._noField = elById(elData(this._field, 'no-input-id'));
+                               if (this._noField === null) {
+                                       throw new Error("Cannot find 'no' input field for input field '" + fieldId + "'");
+                               }
+                               
+                               this._fields.push(this._noField);
+                       }
+                       
+                       DependencyManager.addDependency(this);
+               }
+       };
+       
+       return Abstract;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js
new file mode 100644 (file)
index 0000000..442e366
--- /dev/null
@@ -0,0 +1,91 @@
+/**
+ * Manages form field dependencies.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Form/Builder/Field/Dependency
+ * @since      3.2
+ */
+define(['Dictionary'], function(Dictionary) {
+       "use strict";
+       
+       /**
+        * list if fields for which event listeners have been registered
+        * @type        {Dictionary}
+        * @private
+        */
+       var _fields = new Dictionary();
+       
+       /**
+        * list of dependencies grouped by the dependent node they belong to
+        * @type        {Dictionary}
+        * @private
+        */
+       var _nodeDependencies = new Dictionary();
+       
+       return {
+               /**
+                * Registers a new form field dependency.
+                * 
+                * @param       {WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract}      dependency      new dependency
+                */
+               addDependency: function(dependency) {
+                       var dependentNode = dependency.getDependentNode();
+                       if (!_nodeDependencies.has(dependentNode.id)) {
+                               _nodeDependencies.set(dependentNode.id, [dependency]);
+                       }
+                       else {
+                               _nodeDependencies.get(dependentNode.id).push(dependency);
+                       }
+                       
+                       var fields = dependency.getFields();
+                       for (var i = 0, length = fields.length; i < length; i++) {
+                               var field = fields[i];
+                               if (!_fields.has(field.id)) {
+                                       _fields.set(field.id, field);
+                                       
+                                       if (field.tagName === 'INPUT' && (field.type === 'checkbox' || field.type === 'radio')) {
+                                               field.addEventListener('change', this.checkDependencies.bind(this));
+                                       }
+                                       else {
+                                               field.addEventListener('input', this.checkDependencies.bind(this));
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Check all dependencies if they are met.
+                */
+               checkDependencies: function() {
+                       var obsoleteNodes = [];
+                       
+                       _nodeDependencies.forEach(function(nodeDependencies, nodeId) {
+                               var dependentNode = elById(nodeId);
+                               
+                               // check if dependent node still exists
+                               if (dependentNode === null) {
+                                       obsoleteNodes.push(dependentNode);
+                                       return;
+                               }
+                               
+                               for (var i = 0, length = nodeDependencies.length; i < length; i++) {
+                                       // if any dependency is met, the element is visible
+                                       if (nodeDependencies[i].checkDependency()) {
+                                               elShow(dependentNode);
+                                               return;
+                                       }
+                               }
+                               
+                               // no node dependencies is met
+                               elHide(dependentNode);
+                       });
+                       
+                       // delete dependencies for removed elements
+                       for (var i = 0, length = obsoleteNodes.length; i < length; i++) {
+                               _nodeDependencies.delete(obsoleteNodes.id);
+                       }
+               }
+       };
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty.js
new file mode 100644 (file)
index 0000000..2c20788
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Form field dependency implementation that requires the value of a field not to be empty.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Form/Builder/Field/Dependency
+ * @see        module:WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract
+ * @since      3.2
+ */
+define(['./Abstract', 'Core'], function(Abstract, Core) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function NonEmpty(dependentElementId, fieldId) {
+               this.init(dependentElementId, fieldId);
+       };
+       Core.inherit(NonEmpty, Abstract, {
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract#checkDependency
+                */
+               checkDependency: function() {
+                       switch (this._field.tagName) {
+                               case 'INPUT':
+                                       switch (this._field.type) {
+                                               case 'checkbox':
+                                                       // TODO: check if working
+                                                       return this._field.checked;
+                                               
+                                               case 'radio':
+                                                       if (this._noField && this._noField.checked) {
+                                                               return false;
+                                                       }
+                                                       
+                                                       return this._field.checked;
+                                               
+                                               default:
+                                                       return this._field.value.trim().length !== 0;
+                                       }
+                               
+                               case 'SELECT':
+                                       // TODO: check if working for multiselect
+                                       return this._field.value.length !== 0;
+                               
+                               case 'TEXTAREA':
+                                       // TODO: check if working
+                                       return this._field.value.trim().length !== 0;
+                       }
+               }
+       });
+       
+       return NonEmpty;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.js
new file mode 100644 (file)
index 0000000..63dae62
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Form field dependency implementation that requires a field to have a certain value.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Form/Builder/Field/Dependency
+ * @see        module:WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract
+ * @since      3.2
+ */
+define(['./Abstract', 'Core'], function(Abstract, Core) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function Value(dependentElementId, fieldId) {
+               this.init(dependentElementId, fieldId);
+       };
+       Core.inherit(Value, Abstract, {
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract#checkDependency
+                */
+               checkDependency: function() {
+                       if (!this._values) {
+                               throw new Error("Values have not been set.");
+                       }
+                       
+                       // do not use `Array.prototype.indexOf()` as use a weak comparision
+                       for (var i = 0, length = this._values.length; i < length; i++) {
+                               console.log(this._values[i], this._field.value);
+                               if (this._values[i] == this._field.value) {
+                                       return true;
+                               }
+                       }
+                       
+                       return false;
+               },
+               
+               /**
+                * Sets the possible values the field may have for the dependency to be met.
+                * 
+                * @param       {array}         values
+                */
+               values: function(values) {
+                       this._values = values;
+               }
+       });
+       
+       return Value;
+});
\ No newline at end of file
index 19830c94dc440f62f7439dae5b6ffb9aa1933874..085bc7126dcb653d6a1d3b2cca6a9d2cc5120906 100644 (file)
@@ -6,6 +6,8 @@ use wcf\system\form\builder\container\FormContainer;
 use wcf\system\form\builder\container\TabFormContainer;
 use wcf\system\form\builder\container\TabMenuFormContainer;
 use wcf\system\form\builder\field\data\CustomFormFieldDataProcessor;
+use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
 use wcf\system\form\builder\field\validation\FormFieldValidationError;
 use wcf\system\form\builder\field\validation\FormFieldValidator;
 use wcf\system\form\builder\field\BooleanFormField;
@@ -78,6 +80,8 @@ class DevtoolsFormBuilderTestForm extends AbstractForm {
                                ->description('wcf.global.description')
                                ->addClass('someSection')
                                ->appendChildren([
+                                       TextFormField::create('name')
+                                               ->label('wcf.global.name'),
                                        TextFormField::create('title')
                                                ->label('wcf.global.title')
                                                ->i18n()
@@ -98,7 +102,7 @@ class DevtoolsFormBuilderTestForm extends AbstractForm {
                                                ->label('Year')
                                                ->options(function() {
                                                        return [
-                                                               '(no selection)',
+                                                               '' => '(no selection)',
                                                                2016 => 2016,
                                                                2017 => 2017,
                                                                2018 => 2018,
@@ -132,7 +136,9 @@ class DevtoolsFormBuilderTestForm extends AbstractForm {
                                                                                ->minimum(10)
                                                                                ->maximum(100)
                                                                                ->value(20)
-                                                                               ->suffix('wcf.acp.option.suffix.days')
+                                                                               ->suffix('wcf.acp.option.suffix.days'),
+                                                                       BooleanFormField::create('isCool')
+                                                                               ->label('Foo and Bar are cool names')
                                                                ])
                                                ),
                                        TabFormContainer::create('tab2')
@@ -140,6 +146,31 @@ class DevtoolsFormBuilderTestForm extends AbstractForm {
                                ])
                ]);
                
+               // add dependencies
+               $this->form->getNodeById('month')
+                       ->addDependency(
+                               NonEmptyFormFieldDependency::create('year')
+                                       ->field($this->form->getNodeById('year'))
+                       )
+                       ->addDependency(
+                               NonEmptyFormFieldDependency::create('name')
+                                       ->field($this->form->getNodeById('name'))
+                       )
+                       ->addDependency(
+                               NonEmptyFormFieldDependency::create('isDisabled')
+                                       ->field($this->form->getNodeById('isDisabled'))
+                       );
+               
+               $this->form->getNodeById('isCool')
+                       ->addDependency(
+                               ValueFormFieldDependency::create('name')
+                                       ->field($this->form->getNodeById('name'))
+                                       ->values([
+                                               'Foo',
+                                               'Bar'
+                                       ])
+                       );
+               
                $this->form->build();
                
                $this->form->getDataHandler()->add(new CustomFormFieldDataProcessor('isDisabledToString', function(IFormDocument $document, array $parameters) {
index def537f39afe2df89835299d6e5a944059116931..cd1ab3bbc781de55e0e787695046309388675cbf 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 namespace wcf\system\form\builder;
-use wcf\system\form\builder\field\dependency\IFormFieldDependency;
 
 /**
  * Represents an element of a form that can have a description, a label and dependencies.
@@ -12,19 +11,6 @@ use wcf\system\form\builder\field\dependency\IFormFieldDependency;
  * @since      3.2
  */
 interface IFormElement extends IFormNode {
-       /**
-        * Adds a dependency on the value of a `IFormField` so that this element is
-        * only available if the field satisfies the given dependency and returns
-        * this element.
-        * 
-        * This method is expected to set the dependent element of the given dependency
-        * to this element.
-        * 
-        * @param       IFormFieldDependency            $dependency     added element dependency
-        * @return      static                                          this element
-        */
-       public function addDependency(IFormFieldDependency $dependency);
-       
        /**
         * Sets the description of this element using the given language item
         * and returns this element. If `null` is passed, the element description
@@ -52,17 +38,6 @@ interface IFormElement extends IFormNode {
         */
        public function getLabel();
        
-       /**
-        * Returns `true` if this element has a dependency with the given id and
-        * returns `false` otherwise.
-        * 
-        * @param       string          $dependencyId   id of the checked dependency
-        * @return      bool
-        * 
-        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid
-        */
-       public function hasDependency($dependencyId);
-       
        /**
         * Sets the label of this element using the given language item and
         * returns this element. If `null` is passed, the element label is
@@ -75,14 +50,4 @@ interface IFormElement extends IFormNode {
         * @throws      \InvalidArgumentException       if the given label is no string or otherwise is invalid
         */
        public function label($languageItem = null, array $variables = []);
-       
-       /**
-        * Removes the dependency with the given id and returns this element.
-        * 
-        * @param       string          $dependencyId   id of the removed dependency
-        * @return      static                          this field
-        * 
-        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid or no such dependency exists
-        */
-       public function removeDependency($dependencyId);
 }
index 5d17d781ece5e2d103f074880dd9d4ea09f097de..ca6725ff6d1dbf93bf8308358a08005cc33804dd 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\form\builder;
+use wcf\system\form\builder\field\dependency\IFormFieldDependency;
 
 /**
  * Represents a general form node providing common methods of all nodes.
@@ -21,6 +22,19 @@ interface IFormNode {
         */
        public function addClass($class);
        
+       /**
+        * Adds a dependency on the value of a `IFormField` so that this node is
+        * only available if the field satisfies the given dependency and returns
+        * this element.
+        * 
+        * This method is expected to set the dependent node of the given dependency
+        * to this element.
+        * 
+        * @param       IFormFieldDependency    $dependency     added node dependency
+        * @return      static                                  this node
+        */
+       public function addDependency(IFormFieldDependency $dependency);
+       
        /**
         * Adds an additional attribute to this node and returns this node.
         * 
@@ -34,6 +48,13 @@ interface IFormNode {
         */
        public function attribute($name, $value = null);
        
+       /**
+        * Returns `true` if the node's dependencies are met and returns `false` otherwise.
+        *
+        * @return      bool
+        */
+       public function checkDependencies();
+       
        /**
         * Returns the value of the additional attribute of this node with the given name.
         * 
@@ -58,6 +79,13 @@ interface IFormNode {
         */
        public function getClasses();
        
+       /**
+        * Returns all of the node's dependencies.
+        * 
+        * @return      IFormFieldDependency[]          node's dependencies
+        */
+       public function getDependencies();
+       
        /**
         * Returns the form document this node belongs to.
         * 
@@ -125,6 +153,17 @@ interface IFormNode {
         */
        public function hasClass($class);
        
+       /**
+        * Returns `true` if this node has a dependency with the given id and
+        * returns `false` otherwise.
+        * 
+        * @param       string          $dependencyId   id of the checked dependency
+        * @return      bool
+        * 
+        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid
+        */
+       public function hasDependency($dependencyId);
+       
        /**
         * Sets the id of the node.
         * 
@@ -161,6 +200,16 @@ interface IFormNode {
         */
        public function removeClass($class);
        
+       /**
+        * Removes the dependency with the given id and returns this node.
+        * 
+        * @param       string          $dependencyId   id of the removed dependency
+        * @return      static                          this node
+        * 
+        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid or no such dependency exists
+        */
+       public function removeDependency($dependencyId);
+       
        /**
         * Validates the node.
         * 
index ea67bd9ec678291e2af9408fe96fb12a9fef704a..55f0d5f9dd94c1519bfb51c45cb21eeeba322efc 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 namespace wcf\system\form\builder;
-use wcf\system\form\builder\field\dependency\IFormFieldDependency;
 use wcf\system\WCF;
 
 /**
@@ -27,31 +26,6 @@ trait TFormElement {
         */
        protected $__label;
        
-       /**
-        * dependencies of this element
-        * @var IFormFieldDependency[]
-        */
-       protected $dependencies = [];
-       
-       /**
-        * Adds a dependency on the value of a `IFormField` so that this element is
-        * only available if the field satisfies the given dependency and returns
-        * this element.
-        *
-        * This method is expected to set the dependent element of the given dependency
-        * to this element.
-        *
-        * @param       IFormFieldDependency            $dependency     added element dependency
-        * @return      static                                          this element
-        */
-       public function addDependency(IFormFieldDependency $dependency) {
-               $this->dependencies[] = $dependency;
-               
-               $dependency->dependentElement($this);
-               
-               return $this;
-       }
-       
        /**
         * Sets the description of this element using the given language item
         * and returns this element. If `null` is passed, the element description
@@ -93,41 +67,22 @@ trait TFormElement {
        
        /**
         * Returns the label of this element or `null` if no label has been set.
-        *
+        * 
         * @return      null|string     element label
         */
        public function getLabel() {
                return $this->__label;
        }
        
-       /**
-        * Returns `true` if this element has a dependency with the given id and
-        * returns `false` otherwise.
-        * 
-        * @param       string          $dependencyId   id of the checked dependency
-        * @return      bool
-        * 
-        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid
-        */
-       public function hasDependency($dependencyId) {
-               foreach ($this->dependencies as $dependency) {
-                       if ($dependency->getId() === $dependencyId) {
-                               return true;
-                       }
-               }
-               
-               return false;
-       }
-       
        /**
         * Sets the label of this element using the given language item and
         * returns this element. If `null` is passed, the element label is
         * removed.
-        *
+        * 
         * @param       null|string     $languageItem   language item containing the element label or `null` to unset label
         * @param       array           $variables      additional variables used when resolving the language item
         * @return      static                          this element
-        *
+        * 
         * @throws      \InvalidArgumentException       if the given label is no string or otherwise is invalid
         */
        public function label($languageItem = null, array $variables = []) {
@@ -148,24 +103,4 @@ trait TFormElement {
                
                return $this;
        }
-       
-       /**
-        * Removes the dependency with the given id and returns this element.
-        * 
-        * @param       string          $dependencyId   id of the removed dependency
-        * @return      static                          this field
-        * 
-        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid or no such dependency exists
-        */
-       public function removeDependency($dependencyId) {
-               foreach ($this->dependencies as $key => $dependency) {
-                       if ($dependency->getId() === $dependencyId) {
-                               unset($this->dependencies[$key]);
-                               
-                               return $this;
-                       }
-               }
-               
-               throw new \InvalidArgumentException("Unknown dependency with id '{$dependencyId}'.");
-       }
 }
index c399dc1b2db3a3085ed75ff68b5eeb6369ca59e1..37ba7bb300b968cc2e32b4b6626467ca2555e11d 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\form\builder;
+use wcf\system\form\builder\field\dependency\IFormFieldDependency;
 
 /**
  * Provides default implementations of `IFormNode` methods.
@@ -29,6 +30,12 @@ trait TFormNode {
         */
        protected $__id;
        
+       /**
+        * dependencies of this node
+        * @var IFormFieldDependency[]
+        */
+       protected $dependencies = [];
+       
        /**
         * is `true` if node has already been populated and is `false` otherwise 
         * @var bool
@@ -59,6 +66,25 @@ trait TFormNode {
                return $this;
        }
        
+       /**
+        * Adds a dependency on the value of a `IFormField` so that this node is
+        * only available if the field satisfies the given dependency and returns
+        * this node.
+        * 
+        * This method is expected to set the dependent node of the given dependency
+        * to this node.
+        * 
+        * @param       IFormFieldDependency            $dependency     added node dependency
+        * @return      static                                          this node
+        */
+       public function addDependency(IFormFieldDependency $dependency) {
+               $this->dependencies[] = $dependency;
+               
+               $dependency->dependentNode($this);
+               
+               return $this;
+       }
+       
        /**
         * Adds an additional attribute to this node and returns this node.
         *
@@ -82,6 +108,21 @@ trait TFormNode {
                return $this;
        }
        
+       /**
+        * Returns `true` if the node's dependencies are met and returns `false` otherwise.
+        *
+        * @return      bool
+        */
+       public function checkDependencies() {
+               foreach ($this->dependencies as $dependency) {
+                       if (!$dependency->checkDependency()) {
+                               return false;
+                       }
+               }
+               
+               return true;
+       }
+       
        /**
         * Returns the value of the additional attribute of this node with the given name.
         * 
@@ -116,6 +157,15 @@ trait TFormNode {
                return $this->__classes;
        }
        
+       /**
+        * Returns all of the node's dependencies.
+        * 
+        * @return      IFormFieldDependency[]          node's dependencies
+        */
+       public function getDependencies() {
+               return $this->dependencies;
+       }
+       
        /**
         * Returns the form document this node belongs to.
         *
@@ -194,6 +244,25 @@ trait TFormNode {
                return array_search($class, $this->__classes) !== false;
        }
        
+       /**
+        * Returns `true` if this node has a dependency with the given id and
+        * returns `false` otherwise.
+        * 
+        * @param       string          $dependencyId   id of the checked dependency
+        * @return      bool
+        * 
+        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid
+        */
+       public function hasDependency($dependencyId) {
+               foreach ($this->dependencies as $dependency) {
+                       if ($dependency->getId() === $dependencyId) {
+                               return true;
+                       }
+               }
+               
+               return false;
+       }
+       
        /**
         * Sets the id of the node.
         *
@@ -258,13 +327,33 @@ trait TFormNode {
                return $this;
        }
        
+       /**
+        * Removes the dependency with the given id and returns this node.
+        * 
+        * @param       string          $dependencyId   id of the removed dependency
+        * @return      static                          this field
+        * 
+        * @throws      \InvalidArgumentException       if the given id is no string or otherwise invalid or no such dependency exists
+        */
+       public function removeDependency($dependencyId) {
+               foreach ($this->dependencies as $key => $dependency) {
+                       if ($dependency->getId() === $dependencyId) {
+                               unset($this->dependencies[$key]);
+                               
+                               return $this;
+                       }
+               }
+               
+               throw new \InvalidArgumentException("Unknown dependency with id '{$dependencyId}'.");
+       }
+       
        /**
         * Creates a new element with the given id.
         * 
         * @param       string          $id     node id
         * @return      static
         * 
-        * @throws      \InvalidArgumentException       if the given id is no string, already used by another element, or otherwise is invalid
+        * @throws      \InvalidArgumentException       if the given id is no string, already used by another node, or otherwise is invalid
         */
        public static function create($id) {
                return (new static)->id($id);
index 940a78dc0b20007b93d52958bb33f95173fb541d..9dbc5cf845e2113139ebcb575ac061a32dc00bfe 100644 (file)
@@ -282,8 +282,16 @@ trait TFormParentNode {
         * nodes are valid. A `IFormField` object is valid if its value is valid.
         */
        public function validate() {
-               foreach ($this->children() as $child) {
-                       $child->validate();
+               if ($this->checkDependencies()) {
+                       foreach ($this->children() as $child) {
+                               // call `checkDependencies()` on form fields here so that their validate
+                               // method does not have to do it
+                               if ($child instanceof IFormField && !$child->checkDependencies()) {
+                                       continue;
+                               }
+                               
+                               $child->validate();
+                       }
                }
        }
        
index 4f8a1689544c9222b67a0f93ba3ce3051d601f1f..f522b6251938d2c767113c65820a9b74c4180a01 100644 (file)
@@ -3,6 +3,7 @@ namespace wcf\system\form\builder\field\data;
 use wcf\system\form\builder\field\IFormField;
 use wcf\system\form\builder\IFormDocument;
 use wcf\system\form\builder\IFormNode;
+use wcf\system\form\builder\IFormParentNode;
 
 /**
  * Default field data processor that maps the form fields to entries in
@@ -21,13 +22,22 @@ class DefaultFormFieldDataProcessor implements IFormFieldDataProcessor {
         */
        public function __invoke(IFormDocument $document, array $parameters) {
                $parameters['data'] = [];
-               /** @var IFormNode $node */
-               foreach ($document->getIterator() as $node) {
-                       if ($node instanceof IFormField && $node->hasSaveValue()) {
-                               $parameters['data'][$node->getId()] = $node->getSaveValue();
-                       }
-               }
+               
+               $this->getData($document, $parameters['data']);
                
                return $parameters;
        }
+       
+       protected function getData(IFormNode $node, array &$data) {
+               if ($node->checkDependencies()) {
+                       if ($node instanceof IFormParentNode) {
+                               foreach ($node as $childNode) {
+                                       $this->getData($childNode, $data);
+                               }
+                       }
+                       else if ($node instanceof IFormField && $node->hasSaveValue()) {
+                               $data[$node->getId()] = $node->getSaveValue();;
+                       }
+               }
+       }
 }
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/dependency/AbstractFormFieldDependency.class.php b/wcfsetup/install/files/lib/system/form/builder/field/dependency/AbstractFormFieldDependency.class.php
new file mode 100644 (file)
index 0000000..99797e1
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+namespace wcf\system\form\builder\field\dependency;
+use wcf\system\form\builder\field\IFormField;
+use wcf\system\form\builder\IFormNode;
+use wcf\system\WCF;
+
+/**
+ * Abstract implementation of a form field dependency.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Form\Builder\Field\Dependency
+ * @since      3.2
+ */
+abstract class AbstractFormFieldDependency implements IFormFieldDependency {
+       /**
+        * node whose availability depends on the value of a field
+        * @var IFormNode
+        */
+       protected $__dependentNode;
+       
+       /**
+        * field the availability of the node dependents on
+        * @var IFormField
+        */
+       protected $__field;
+       
+       /**
+        * id of the dependency
+        * @var string
+        */
+       protected $__id;
+       
+       /**
+        * name of the template containing the dependency JavaScript code
+        * @var null|string
+        */
+       protected $templateName;
+       
+       /**
+        * @inheritDoc
+        */
+       public function dependentNode(IFormNode $node) {
+               $this->__dependentNode = $node;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function field(IFormField $field) {
+               $this->__field = $field;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getDependentNode() {
+               return $this->__dependentNode;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getField() {
+               return $this->__field;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getId() {
+               return $this->__id;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getHtml() {
+               if ($this->templateName === null) {
+                       throw new \LogicException("Template name is not set.");
+               }
+               
+               return WCF::getTPL()->fetch($this->templateName, 'wcf', [
+                       'dependency' => $this
+               ], true);
+       }
+       
+       /**
+        * Sets the id of this dependency and returns this dependency.
+        * 
+        * @param       string          $id             id of the dependency
+        * @return      static          $this           this dependency
+        * 
+        * @throws      \InvalidArgumentException       if given id no string or otherwise invalid
+        */
+       protected function id($id) {
+               if (!is_string($id)) {
+                       throw new \InvalidArgumentException("Given id is no string, " . gettype($id) . " given.");
+               }
+               
+               if (preg_match('~^[a-z][A-z0-9-]*$~', $id) !== 1) {
+                       throw new \InvalidArgumentException("Invalid id '{$id}' given.");
+               }
+               
+               $this->__id = $id;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function create($id) {
+               return (new static)->id($id);
+       }
+}
index 8fe8b1def537c570382cd9257e3c43f1c834a219..a590d67055e0e061f93e35cf67ff65da155dc161 100644 (file)
@@ -1,10 +1,10 @@
 <?php
 namespace wcf\system\form\builder\field\dependency;
 use wcf\system\form\builder\field\IFormField;
-use wcf\system\form\builder\IFormElement;
+use wcf\system\form\builder\IFormNode;
 
 /**
- * Represents a dependency of one field on (the value of) another field.
+ * Represents a dependency of one node on (the value of) a field.
  * 
  * @author     Matthias Schmidt
  * @copyright  2001-2018 WoltLab GmbH
@@ -22,15 +22,15 @@ interface IFormFieldDependency {
        public function checkDependency();
        
        /**
-        * Sets the element whose availability depends on the value of a field.
+        * Sets the node whose availability depends on the value of a field.
         * 
-        * @param       IFormElement    $element        depending element
-        * @return      static                          this dependency
+        * @param       IFormNode       $node   depending node
+        * @return      static                  this dependency
         */
-       public function dependentElement(IFormElement $element);
+       public function dependentNode(IFormNode $node);
        
        /**
-        * Sets the field the availability of the element dependents on.
+        * Sets the field the availability of the node dependents on.
         * 
         * @param       IFormField      $field          dependent field
         * @return      static                          this dependency
@@ -38,32 +38,32 @@ interface IFormFieldDependency {
        public function field(IFormField $field);
        
        /**
-        * Returns the JavaScript code required to ensure this dependency in the template.
-        *
-        * @return      string          dependency JavaScript code
+        * Returns the node whose availability depends on the value of a field.
+        * 
+        * @return      IFormNode       dependent node
         */
-       public function getHtml();
+       public function getDependentNode();
        
        /**
-        * Returns the id of the dependency.
+        * Returns the field the availability of the element dependents on.
         * 
-        * @return      string          id of the dependency 
+        * @return      IFormField      field controlling element availability
         */
-       public function getId();
+       public function getField();
        
        /**
-        * Returns the element whose availability depends on the value of a field.
+        * Returns the JavaScript code required to ensure this dependency in the template.
         * 
-        * @return      IFormElement    depending element
+        * @return      string          dependency JavaScript code
         */
-       public function getDependentElement();
+       public function getHtml();
        
        /**
-        * Returns the field the availability of the element dependents on.
+        * Returns the id of this dependency.
         * 
-        * @return      IFormField      dependent field
+        * @return      string          id of the dependency 
         */
-       public function getField();
+       public function getId();
        
        /**
         * Creates a new dependency with the given id.
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/dependency/NonEmptyFormFieldDependency.class.php b/wcfsetup/install/files/lib/system/form/builder/field/dependency/NonEmptyFormFieldDependency.class.php
new file mode 100644 (file)
index 0000000..7ea2d9a
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\system\form\builder\field\dependency;
+
+/**
+ * Represents a dependency that requires the value of a field not to be empty.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Form\Builder\Field\Dependency
+ * @since      3.2
+ */
+class NonEmptyFormFieldDependency extends AbstractFormFieldDependency {
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__nonEmptyFormFieldDependency';
+       
+       /**
+        * @inheritDoc
+        */
+       public function checkDependency() {
+               return !empty($this->getField()->getValue());
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/dependency/ValueFormFieldDependency.class.php b/wcfsetup/install/files/lib/system/form/builder/field/dependency/ValueFormFieldDependency.class.php
new file mode 100644 (file)
index 0000000..0b06f0e
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace wcf\system\form\builder\field\dependency;
+
+/**
+ * Represents a dependency that requires that requires a field to have a certain value.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Form\Builder\Field\Dependency
+ * @since      3.2
+ */
+class ValueFormFieldDependency extends AbstractFormFieldDependency {
+       /**
+        * possible values the field may have for the dependency to be met
+        * @var null|array
+        */
+       protected $__values;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__valueFormFieldDependency';
+       
+       /**
+        * @inheritDoc
+        */
+       public function checkDependency() {
+               return !empty($this->getField()->getValue());
+       }
+       
+       /**
+        * Returns the possible values the field may have for the dependency to be met.
+        * 
+        * @return      array                           possible field values
+        * 
+        * @throws      \BadMethodCallException         if no values have been set
+        */
+       public function getValues() {
+               if ($this->__values === null) {
+                       throw new \BadMethodCallException("Values have not been set for dependency '{$this->getId()}' on node '{$this->getDependentNode()->getId()}'.");
+               }
+               
+               return $this->__values;
+       }
+       
+       /**
+        * Sets the possible values the field may have for the dependency to be met.
+        * 
+        * @param       array           $values         possible field values
+        * @return      static          $this           this dependency
+        * 
+        * @throws      \InvalidArgumentException       if given values are invalid
+        */
+       public function values(array $values) {
+               if (empty($values)) {
+                       throw new \InvalidArgumentException("Given values are empty.");
+               }
+               foreach ($values as $value) {
+                       if (!is_string($value) && !is_numeric($value)) {
+                               throw new \InvalidArgumentException("Values contains invalid value of type '" . gettype($value) . "', only strings or numbers are allowed.");
+                       }
+               }
+               
+               $this->__values = $values;
+               
+               return $this;
+       }
+}