diff --git a/css/backoffice/components/_form.scss b/css/backoffice/components/_form.scss index dd64a26b8..5023af49a 100644 --- a/css/backoffice/components/_form.scss +++ b/css/backoffice/components/_form.scss @@ -47,7 +47,7 @@ align-items: center; } -.form[aria-busy="true"] { +.turbo-refreshing{ opacity: .5; } diff --git a/js/forms/custom-elements/choices.js b/js/forms/custom-elements/choices.js index fb70acd48..eb1dd9133 100644 --- a/js/forms/custom-elements/choices.js +++ b/js/forms/custom-elements/choices.js @@ -1,4 +1,10 @@ class ChoicesElement extends HTMLSelectElement { + + // register the custom element + static { + customElements.define('choices-element', ChoicesElement, {extends: 'select'}); + } + plugins = []; connectedCallback() { @@ -36,4 +42,4 @@ class ChoicesElement extends HTMLSelectElement { } } -customElements.define('choices-element', ChoicesElement, {extends: 'select'}); + diff --git a/js/forms/custom-elements/form.js b/js/forms/custom-elements/form.js new file mode 100644 index 000000000..e59c7af50 --- /dev/null +++ b/js/forms/custom-elements/form.js @@ -0,0 +1,90 @@ +class FormElement extends HTMLFormElement +{ + static #TURBO_REFRESHING_CLASS = 'turbo-refreshing'; + static #TURBO_TRIGGER_FIELD = '_turbo_trigger'; + + #aFormBlockDataTransmittedData = {}; + + // register the custom element + static { + customElements.define('itop-form-element', FormElement, {extends: 'form'}); + } + + TriggerTurbo(oElement) { + + // Get the name and id of the element triggering turbo + const sName = oElement.getAttribute('name'); + const sId = oElement.getAttribute('id'); + + if(FormElement.IsCheckbox(oElement) || this.#aFormBlockDataTransmittedData[sName] !== oElement.value) { + + // Refresh UI + this.#StartRefreshingUI(sId); + + // Pre Submit + this.#PreSubmitTurboForm(sName); + + // Submit + oElement.form.requestSubmit(); + + // Post Submit + this.#PostSubmitTurboForm(sName) + + this.#aFormBlockDataTransmittedData[sName] = oElement.value; + } + + } + + /** + * Start refreshing UI. + * + * @param sId + * @constructor + */ + #StartRefreshingUI(sId) + { + Array.from(this.querySelectorAll(`.ibo-content-block[data-impacted-by*="${sId}"]`)).forEach(block => { + block.classList.add(FormElement.#TURBO_REFRESHING_CLASS); + }); + } + + /** + * Pre submit the form. + * Set the turbo trigger field in the form and disable validation + * + * @param sName + * @constructor + */ + #PreSubmitTurboForm(sName) + { + this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = sName; + this.setAttribute('novalidate', true); + } + + /** + * Post submit the form. + * Reset the turbo trigger field and restore form validation. + * + * @param sName + * @constructor + */ + #PostSubmitTurboForm(sName) + { + this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = null; + this.removeAttribute('novalidate'); + } + + /** + * + * @param oElement + * @returns {boolean} + */ + static IsCheckbox (oElement) + { + return oElement instanceof HTMLInputElement + && oElement.getAttribute('type') === 'checkbox' + } + +} + + diff --git a/js/forms/forms.js b/js/forms/forms.js deleted file mode 100644 index 20700c7fc..000000000 --- a/js/forms/forms.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @copyright Copyright (C) 2010-2025 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -$aFormBlockDataTransmittedData = {}; - -function triggerTurbo(el) { - - const name = el.getAttribute('name'); - - if(isCheckbox(el) || $aFormBlockDataTransmittedData[name] !== el.value) { - let sFormName = el.form.getAttribute("name"); - el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = el.getAttribute('name'); - el.form.setAttribute('novalidate', true); - el.form.requestSubmit(); - el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = null; - el.form.removeAttribute('novalidate'); - $aFormBlockDataTransmittedData[name] = el.value; - } - -} - -function isCheckbox (element) { - return element instanceof HTMLInputElement - && element.getAttribute('type') === 'checkbox' -} diff --git a/sources/Application/WebPage/iTopWebPage.php b/sources/Application/WebPage/iTopWebPage.php index a79bf6c61..7e123038a 100644 --- a/sources/Application/WebPage/iTopWebPage.php +++ b/sources/Application/WebPage/iTopWebPage.php @@ -165,8 +165,6 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage { parent::InitializeLinkedScripts(); - $this->LinkScriptFromAppRoot('js/forms/forms.js'); - // Used by forms $this->LinkScriptFromAppRoot('js/leave_handler.js'); @@ -182,6 +180,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->LinkScriptFromAppRoot('node_modules/selectize-plugin-a11y/selectize-plugin-a11y.js'); $this->LinkScriptFromAppRoot('js/jquery.multiselect.js'); $this->LinkScriptFromAppRoot('node_modules/tom-select/dist/js/tom-select.complete.min.js'); + $this->LinkScriptFromAppRoot('js/forms/custom-elements/form.js'); $this->LinkScriptFromAppRoot('js/forms/custom-elements/choices.js'); $this->LinkScriptFromAppRoot('js/forms/custom-elements/oql.js'); $this->LinkScriptFromAppRoot('js/forms/custom-elements/collection.js'); diff --git a/sources/Forms/Block/AbstractFormBlock.php b/sources/Forms/Block/AbstractFormBlock.php index c69ed537e..12c035fdc 100644 --- a/sources/Forms/Block/AbstractFormBlock.php +++ b/sources/Forms/Block/AbstractFormBlock.php @@ -391,9 +391,19 @@ abstract class AbstractFormBlock implements IFormBlock * * @return bool */ - public function ImpactDependentsBlocks(): bool + public function IsImpactingBlocks(): bool { - return $this->oIORegister->ImpactDependentsBlocks(); + return $this->oIORegister->IsImpactingBlocks(); + } + + /** + * Return the dependencies blocks. + * + * @return array + */ + public function GetImpactedBlocks(): array + { + return $this->oIORegister->GetImpactedBlocks(); } /** diff --git a/sources/Forms/FormBuilder/FormBuilder.php b/sources/Forms/FormBuilder/FormBuilder.php index 97d9f2034..3dcfc71b2 100644 --- a/sources/Forms/FormBuilder/FormBuilder.php +++ b/sources/Forms/FormBuilder/FormBuilder.php @@ -100,6 +100,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate $this->builder->add('_turbo_trigger', HiddenType::class, [ 'prevent_form_build' => true, 'mapped' => false, + 'priority' => 1 ]); } } diff --git a/sources/Forms/FormBuilder/FormTypeExtension.php b/sources/Forms/FormBuilder/FormTypeExtension.php index a3cb3829a..2d2a35bd6 100644 --- a/sources/Forms/FormBuilder/FormTypeExtension.php +++ b/sources/Forms/FormBuilder/FormTypeExtension.php @@ -59,7 +59,8 @@ class FormTypeExtension extends AbstractTypeExtension $view->vars['form_block_class'] = $options['form_block_class']; $oFormBlock = $options['form_block']; - $view->vars['trigger_form_submit_on_modify'] = $oFormBlock->ImpactDependentsBlocks(); + $view->vars['trigger_form_submit_on_modify'] = $oFormBlock->IsImpactingBlocks(); + $view->vars['impacted_by'] = array_keys( $oFormBlock->GetImpactedBlocks()); } } } diff --git a/sources/Forms/IO/FormOutput.php b/sources/Forms/IO/FormOutput.php index 9e4f335c7..361ec6929 100644 --- a/sources/Forms/IO/FormOutput.php +++ b/sources/Forms/IO/FormOutput.php @@ -49,7 +49,6 @@ class FormOutput extends AbstractFormIO */ public function ConvertValue(mixed $oData): mixed { - IssueLog::Error($this->GetName().' array:'.$this->IsArray()); if ($this->IsArray()) { return $this->ConvertArrayValue($oData); } else { diff --git a/sources/Forms/Register/IORegister.php b/sources/Forms/Register/IORegister.php index 99cfd3ab8..dd68c4233 100644 --- a/sources/Forms/Register/IORegister.php +++ b/sources/Forms/Register/IORegister.php @@ -261,7 +261,7 @@ class IORegister * * @return bool */ - public function ImpactDependentsBlocks(): bool + public function IsImpactingBlocks(): bool { /** @var FormOutput $oFormOutput */ foreach ($this->aOutputs as $oFormOutput) { @@ -273,6 +273,27 @@ class IORegister return false; } + /** + * Return the dependencies blocks. + * + * @return array + */ + public function GetImpactedBlocks(): array + { + $aBlocks = []; + + /** @var FormInput $oFormInput */ + foreach ($this->aInputs as $oFormInput) { + if ($oFormInput->IsBound()) { + $oBlock = $oFormInput->GetBinding()->oSourceIO->GetOwnerBlock(); + $sId = FormBlockHelper::GetFormId($oBlock); + $aBlocks[$sId] = $oBlock; + } + } + + return $aBlocks; + } + /** * Get bound inputs bindings. * diff --git a/templates/application/forms/itop_console_layout.html.twig b/templates/application/forms/itop_console_layout.html.twig index 583c00e1d..4da4a7d84 100644 --- a/templates/application/forms/itop_console_layout.html.twig +++ b/templates/application/forms/itop_console_layout.html.twig @@ -1,14 +1,40 @@ {% use "application/forms/itop_base_layout.html.twig" %} -{# Widgets #} +{%- block form_start -%} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ["GET", "POST"] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = "POST" %} + {%- endif -%} +
+ {%- if form_method != method -%} + + {%- endif -%} +{%- endblock form_start -%} + +{%- block form_end -%} + {%- if not render_rest is defined or render_rest -%} + {{ form_rest(form) }} + {%- endif -%} +
+{%- endblock form_end -%} {%- block widget_attributes -%} {{- parent() -}} {% if trigger_form_submit_on_modify %} - onChange="triggerTurbo(this);" + onChange="this.form.TriggerTurbo(this);" {% endif %} {%- endblock widget_attributes -%} +{%- block attributes -%} + {{- parent() -}} + {% if impacted_by is not empty %} + data-impacted-by="{{ impacted_by|join(',') }}" + {% endif %} +{%- endblock attributes -%} + {%- block form_widget_simple -%} {% if type == 'text' %}{% set ibo_class='ibo-input-string' %}{% else %}{% set ibo_class='ibo-input-' ~ type %}{% endif %} {% set attr = attr|merge({class: (attr.class|default('') ~ ' ibo-input ' ~ ibo_class)|trim}) %}