From 0a644c6376f3f2c85730790ad362618970d5d4ac Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 13 Jan 2018 08:05:22 +0100 Subject: [PATCH] Add form field dependency implementation (WIP) See #2509 --- .../acp/templates/__booleanFormField.tpl | 2 +- .../files/acp/templates/__formContainer.tpl | 4 +- .../templates/__formContainerDependencies.tpl | 7 + .../acp/templates/__formFieldDependencies.tpl | 7 + .../files/acp/templates/__formFieldFooter.tpl | 1 + .../files/acp/templates/__formFieldHeader.tpl | 2 +- .../__nonEmptyFormFieldDependency.tpl | 7 + .../acp/templates/__tabFormContainer.tpl | 4 +- .../acp/templates/__tabMenuFormContainer.tpl | 7 +- .../templates/__tabTabMenuFormContainer.tpl | 4 +- .../templates/__valueFormFieldDependency.tpl | 7 + .../Form/Builder/Field/Dependency/Abstract.js | 93 ++++++++++++++ .../Form/Builder/Field/Dependency/Manager.js | 91 +++++++++++++ .../Form/Builder/Field/Dependency/NonEmpty.js | 55 ++++++++ .../Form/Builder/Field/Dependency/Value.js | 51 ++++++++ .../DevtoolsFormBuilderTestForm.class.php | 35 ++++- .../form/builder/IFormElement.class.php | 35 ----- .../system/form/builder/IFormNode.class.php | 49 +++++++ .../form/builder/TFormElement.class.php | 71 +--------- .../system/form/builder/TFormNode.class.php | 91 ++++++++++++- .../form/builder/TFormParentNode.class.php | 12 +- .../DefaultFormFieldDataProcessor.class.php | 22 +++- .../AbstractFormFieldDependency.class.php | 121 ++++++++++++++++++ .../dependency/IFormFieldDependency.class.php | 40 +++--- .../NonEmptyFormFieldDependency.class.php | 25 ++++ .../ValueFormFieldDependency.class.php | 69 ++++++++++ 26 files changed, 771 insertions(+), 141 deletions(-) create mode 100644 wcfsetup/install/files/acp/templates/__formContainerDependencies.tpl create mode 100644 wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl create mode 100644 wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl create mode 100644 wcfsetup/install/files/acp/templates/__valueFormFieldDependency.tpl create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.js create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/dependency/AbstractFormFieldDependency.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/dependency/NonEmptyFormFieldDependency.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/dependency/ValueFormFieldDependency.class.php diff --git a/wcfsetup/install/files/acp/templates/__booleanFormField.tpl b/wcfsetup/install/files/acp/templates/__booleanFormField.tpl index 625b02ed1d..da10fb6550 100644 --- a/wcfsetup/install/files/acp/templates/__booleanFormField.tpl +++ b/wcfsetup/install/files/acp/templates/__booleanFormField.tpl @@ -2,7 +2,7 @@
  1. - isAutofocused()} autofocus{/if}{if $field->isRequired()} required{/if}{if $field->isImmutable()} disabled{/if}{if $field->getValue()} checked{/if}> + isAutofocused()} autofocus{/if}{if $field->isRequired()} required{/if}{if $field->isImmutable()} disabled{/if}{if $field->getValue()} checked{/if}>
  2. diff --git a/wcfsetup/install/files/acp/templates/__formContainer.tpl b/wcfsetup/install/files/acp/templates/__formContainer.tpl index 9bf9dd9217..9be5eb52e0 100644 --- a/wcfsetup/install/files/acp/templates/__formContainer.tpl +++ b/wcfsetup/install/files/acp/templates/__formContainer.tpl @@ -1,4 +1,4 @@ -
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}> +
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}> {if $container->getLabel() !== null} {if $container->getDescription() !== null}
    @@ -12,3 +12,5 @@ {include file='__formContainerChildren'}
    + +{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 index 0000000000..e9daa97fde --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__formContainerDependencies.tpl @@ -0,0 +1,7 @@ +{if !$container->getDependencies()|empty} + +{/if} diff --git a/wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl b/wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl new file mode 100644 index 0000000000..c3525c3250 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__formFieldDependencies.tpl @@ -0,0 +1,7 @@ +{if !$field->getDependencies()|empty} + +{/if} diff --git a/wcfsetup/install/files/acp/templates/__formFieldFooter.tpl b/wcfsetup/install/files/acp/templates/__formFieldFooter.tpl index 3c3d9df472..bb5c687390 100644 --- a/wcfsetup/install/files/acp/templates/__formFieldFooter.tpl +++ b/wcfsetup/install/files/acp/templates/__formFieldFooter.tpl @@ -1,4 +1,5 @@ {include file='__formFieldDescription'} {include file='__formFieldErrors'} + {include file='__formFieldDependencies'} diff --git a/wcfsetup/install/files/acp/templates/__formFieldHeader.tpl b/wcfsetup/install/files/acp/templates/__formFieldHeader.tpl index f0f29615cb..cc3438f97c 100644 --- a/wcfsetup/install/files/acp/templates/__formFieldHeader.tpl +++ b/wcfsetup/install/files/acp/templates/__formFieldHeader.tpl @@ -1,3 +1,3 @@ -
    getClasses()|empty} class="{implode from=$field->getClasses() item='class'}{$class}{/implode}"{/if}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}> +
    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}>
    diff --git a/wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl b/wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl new file mode 100644 index 0000000000..1d119ddb14 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__nonEmptyFormFieldDependency.tpl @@ -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()}' + ); +}); diff --git a/wcfsetup/install/files/acp/templates/__tabFormContainer.tpl b/wcfsetup/install/files/acp/templates/__tabFormContainer.tpl index a1e6392d51..c6e3c62d7d 100644 --- a/wcfsetup/install/files/acp/templates/__tabFormContainer.tpl +++ b/wcfsetup/install/files/acp/templates/__tabFormContainer.tpl @@ -1,3 +1,5 @@ -
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}> +
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}> {include file='__formContainerChildren'}
    + +{include file='__formContainerDependencies'} diff --git a/wcfsetup/install/files/acp/templates/__tabMenuFormContainer.tpl b/wcfsetup/install/files/acp/templates/__tabMenuFormContainer.tpl index 77e79396a2..e5f1176b53 100644 --- a/wcfsetup/install/files/acp/templates/__tabMenuFormContainer.tpl +++ b/wcfsetup/install/files/acp/templates/__tabMenuFormContainer.tpl @@ -1,11 +1,14 @@ -
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}> +
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$container->checkDependencies()} style="display: none;"{/if}{if !$container->checkDependencies()} style="display: none;"{/if}> {include file='__formContainerChildren'}
    + +{include file='__formContainerDependencies'} diff --git a/wcfsetup/install/files/acp/templates/__tabTabMenuFormContainer.tpl b/wcfsetup/install/files/acp/templates/__tabTabMenuFormContainer.tpl index 3c3362edb8..3f84fca1ec 100644 --- a/wcfsetup/install/files/acp/templates/__tabTabMenuFormContainer.tpl +++ b/wcfsetup/install/files/acp/templates/__tabTabMenuFormContainer.tpl @@ -1,4 +1,4 @@ -
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}> +
    getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}>
    + +{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 index 0000000000..695c6d3ef9 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__valueFormFieldDependency.tpl @@ -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 index 0000000000..ad00c5ce4f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract.js @@ -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 + * @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 index 0000000000..442e366e3c --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js @@ -0,0 +1,91 @@ +/** + * Manages form field dependencies. + * + * @author Matthias Schmidt + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @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 index 0000000000..2c207887f0 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/NonEmpty.js @@ -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 + * @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 index 0000000000..63dae62d02 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.js @@ -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 + * @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 diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsFormBuilderTestForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsFormBuilderTestForm.class.php index 19830c94dc..085bc7126d 100644 --- a/wcfsetup/install/files/lib/acp/form/DevtoolsFormBuilderTestForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/DevtoolsFormBuilderTestForm.class.php @@ -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) { diff --git a/wcfsetup/install/files/lib/system/form/builder/IFormElement.class.php b/wcfsetup/install/files/lib/system/form/builder/IFormElement.class.php index def537f39a..cd1ab3bbc7 100644 --- a/wcfsetup/install/files/lib/system/form/builder/IFormElement.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/IFormElement.class.php @@ -1,6 +1,5 @@ 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}'."); - } } diff --git a/wcfsetup/install/files/lib/system/form/builder/TFormNode.class.php b/wcfsetup/install/files/lib/system/form/builder/TFormNode.class.php index c399dc1b2d..37ba7bb300 100644 --- a/wcfsetup/install/files/lib/system/form/builder/TFormNode.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/TFormNode.class.php @@ -1,5 +1,6 @@ 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); diff --git a/wcfsetup/install/files/lib/system/form/builder/TFormParentNode.class.php b/wcfsetup/install/files/lib/system/form/builder/TFormParentNode.class.php index 940a78dc0b..9dbc5cf845 100644 --- a/wcfsetup/install/files/lib/system/form/builder/TFormParentNode.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/TFormParentNode.class.php @@ -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(); + } } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/data/DefaultFormFieldDataProcessor.class.php b/wcfsetup/install/files/lib/system/form/builder/field/data/DefaultFormFieldDataProcessor.class.php index 4f8a168954..f522b62519 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/data/DefaultFormFieldDataProcessor.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/data/DefaultFormFieldDataProcessor.class.php @@ -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 index 0000000000..99797e12a8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/dependency/AbstractFormFieldDependency.class.php @@ -0,0 +1,121 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/dependency/IFormFieldDependency.class.php b/wcfsetup/install/files/lib/system/form/builder/field/dependency/IFormFieldDependency.class.php index 8fe8b1def5..a590d67055 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/dependency/IFormFieldDependency.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/dependency/IFormFieldDependency.class.php @@ -1,10 +1,10 @@ + * @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 index 0000000000..0b06f0ea68 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/dependency/ValueFormFieldDependency.class.php @@ -0,0 +1,69 @@ + + * @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; + } +} -- 2.20.1