diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9c1e661a85e80eef9e5a5dc08086eb84bb56478b --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Calculation Fields + +## What the Module Does +The "form_element_math" module is designed to enhance Drupal forms by introducing a new form element type called "form_element_math." This custom form element allows you to create dynamic and mathematically calculated fields within your forms. It's particularly useful when you need to perform mathematical operations based on the values of other form fields and update the result in real-time. + +## How to Use the New Element + +### 1. Adding the "form_element_math" to a Form +To use the "form_element_math" in your Drupal form, you need to define it in the form array. Here's a basic example of how to do this: + +```php +$form['result'] = [ + '#type' => 'form_element_math', + '#title' => $this->t('Result'), + '#required' => TRUE, + '#evaluation_fields' => '(:first_value + :second_value + :third_value) * :multiple', + '#evaluation_round' => 2, +]; +``` + +In this example, we create a field named 'result' of type 'form_element_math.' It has the following attributes: +- `#title`: The title of the field. +- `#required`: Whether this field is mandatory. +- `#evaluation_fields`: The expression to be evaluated. In this case, it calculates the result by adding `:first_value`, `:second_value`, and `:third_value`, and then multiplying the result by `:multiple`. You can use field names with `:` to reference the values of other form fields. When any of these fields change, the expression is re-evaluated and updates the 'result' field. +- `#evaluation_round`: The number of decimal places to round the result to. + +### 2. Examples of Usage + +**Example 1: Calculate Total Price** +Suppose you have a form for ordering items, and you want to calculate the total price based on the quantity and unit price. You can use the "form_element_math" as follows: + +```php +$form['quantity'] = [ + '#type' => 'textfield', + '#title' => $this->t('Quantity'), +]; +$form['unit_price'] = [ + '#type' => 'textfield', + '#title' => $this->t('Unit Price'), +]; +$form['total_price'] = [ + '#type' => 'form_element_math', + '#title' => $this->t('Total Price'), + '#evaluation_fields' => ':quantity * :unit_price', + '#evaluation_round' => 2, +]; +``` + +Now, when users enter the quantity and unit price, the total price is automatically calculated and displayed. + +**Example 2: Calculate BMI (Body Mass Index)** +Imagine you have a health assessment form, and you want to calculate the Body Mass Index (BMI) based on a person's weight and height. You can achieve this with the "form_element_math" element: + +```php +$form['weight'] = [ + '#type' => 'textfield', + '#title' => $this->t('Weight (kg)'), +]; +$form['height'] = [ + '#type' => 'textfield', + '#title' => $this->t('Height (m)'), +]; +$form['bmi'] = [ + '#type' => 'form_element_math', + '#title' => $this->t('BMI'), + '#evaluation_fields' => ':weight / (:height * :height)', + '#evaluation_round' => 2, +]; +``` + +Now, as users input their weight and height, the BMI is calculated and displayed, providing immediate health feedback. + +With the "form_element_math" module, you can easily incorporate dynamic calculations into your forms, making them more interactive and informative for users. diff --git a/README.txt b/README.txt deleted file mode 100644 index c3d57b0aa6317f2988c375c2ddaf36e3d328f954..0000000000000000000000000000000000000000 --- a/README.txt +++ /dev/null @@ -1 +0,0 @@ -Calculation Fields diff --git a/calculation_fields.info.yml b/calculation_fields.info.yml index 03b2b086f709a89c2afe38be1aba6f3aace91150..1c1f6c802c9710482d9f823c8ee13b935af98868 100644 --- a/calculation_fields.info.yml +++ b/calculation_fields.info.yml @@ -1,5 +1,6 @@ name: Calculation Fields type: module -description: Provides additional functionality for the site. +description: Provides additional form element that can evaluation math expressions. package: Fields core_version_requirement: ^9 || ^10 +project: 'drupal/calculation_fields' diff --git a/calculation_fields.libraries.yml b/calculation_fields.libraries.yml index 447c36a0ec9f0b304be88dceb2f6411b2a679da0..ed539449bc984e23336e08446c6ab82c5b51321a 100644 --- a/calculation_fields.libraries.yml +++ b/calculation_fields.libraries.yml @@ -1,12 +1,13 @@ -# Custom module library for general purposes. calculation_fields: js: js/calculation-fields.js: {} + css: + component: + css/calculation-fields.css: { } dependencies: - core/drupal - calculation_fields/mathjs -# Third-party library (CDN). mathjs: remote: https://mathjs.org/ version: 11.11.1 diff --git a/calculation_fields.module b/calculation_fields.module index b3d9bbc7f3711e882119cd6b3af051245d859d04..79cb510fd0007543a8b3a60eae95d197dcd732b5 100644 --- a/calculation_fields.module +++ b/calculation_fields.module @@ -1 +1,20 @@ <?php + +/** + * Implements hook_theme(). + */ +function calculation_fields_theme($existing, $type, $theme, $path) { + return [ + 'form_math_markup_element' => [ + 'variables' => [ + 'round' => NULL, + 'formContext' => NULL, + 'fields' => [], + 'expression' => '', + 'previewText' => NULL, + 'textExpression' => NULL, + 'name' => [], + ], + ], + ]; +} diff --git a/calculation_fields.routing.yml b/calculation_fields.routing.yml deleted file mode 100644 index 193ff55ed585a20892415d7334492d3e6b710be4..0000000000000000000000000000000000000000 --- a/calculation_fields.routing.yml +++ /dev/null @@ -1,7 +0,0 @@ -calculation_fields.example: - path: '/calculation-fields/example' - defaults: - _title: 'Example' - _form: 'Drupal\calculation_fields\Form\ExampleForm' - requirements: - _permission: 'access content' diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..561275a508e0a4ebeaabc6953b5be062ded3ed4c --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "drupal/calculation_fields", + "description": "Extends Drupal form element API adding new form element type to evaluation math expression using math.js and symfony/expression-language", + "type": "drupal-module", + "autoload": { + "psr-4": { + "Drupal\\calculation_fields\\": "src/" + } + }, + "require": { + "symfony/expression-language": "^6.3" + } +} diff --git a/css/calculation-fields.css b/css/calculation-fields.css new file mode 100644 index 0000000000000000000000000000000000000000..fe3d5d4cd285088158c462492958fd2ab56805d7 --- /dev/null +++ b/css/calculation-fields.css @@ -0,0 +1,10 @@ +.form-math-markup-element .js-math-result, +.form-math-markup-element-calculated .js-math-preview, +.js-math-result-base { + display: none; +} + +.form-math-markup-element .js-math-preview, +.form-math-markup-element-calculated .js-math-result { + display: block; +} diff --git a/js/calculation-fields.js b/js/calculation-fields.js index f6db96f38ea38ce91af91d3432a72c042a75d2f8..7e04ec35f098f403429ba60cb27a12330d747d63 100644 --- a/js/calculation-fields.js +++ b/js/calculation-fields.js @@ -1,36 +1,84 @@ +/** + * @file + * Behaviors for the form math custom element. + */ + ((Drupal, math) => { Drupal.behaviors.formMathElementCalculator = { attach: () => { + const previousValue = {}; + + const updateResult = (el, value) =>{ + if (el.tagName === 'INPUT') { + el.value = value; + el.dispatchEvent(new Event('change')); + return; + } + const markupEl = el.querySelector('.form-math-markup-element'); + if (markupEl === null) { + return; + } + const expressionEvaluated = markupEl + .querySelector('.js-math-result-base') + .innerHTML.replace('[RESULT]', value); + + markupEl.querySelector('.js-math-result-input').value = expressionEvaluated; + markupEl.querySelector('.js-math-result').innerHTML = expressionEvaluated; + if (value !== "") { + markupEl.classList.add('form-math-markup-element-calculated'); + } + else { + markupEl.classList.remove('form-math-markup-element-calculated'); + } + } - function calc(fieldElement) { - // fieldElement.classList.add('built'); - const fieldNames = fieldElement.dataset.evaluateFields.split(' '); + const calc = (fieldElement) => { + const fields = fieldElement.dataset.evaluateFields.split(' '); const expression = fieldElement.dataset.evaluateExpression; const formContext = fieldElement.dataset.evaluateContext; - const deepFields = JSON.parse(fieldElement.dataset.evaluateDeepFields) || {}; - - function handleFieldChange (e) { - const convertedExpression = fieldNames.reduce((exp, field) => { - const currentElement = document.querySelector(`input[name="${field}"], select[name="${field}"]`); - let currentFieldValue; - if (currentElement === null && deepFields[field] !== undefined) { - currentFieldValue = deepFields[field]; - } - else { - currentFieldValue = currentElement.value; - } - if (currentFieldValue !== '') { - return exp.replaceAll(`:${field}`, currentFieldValue); - } - return exp; - }, expression); + const deepFields = JSON.parse(fieldElement.dataset.evaluateDeepFields || "{}"); + const fieldEvaluateRound = fieldElement.dataset.evaluateRound || null; + + const buildExpression = (exp, field) => { + const currentElement = document.querySelector(`input[name="${field}"], select[name="${field}"]`); + let currentFieldValue; + if (currentElement === null && deepFields[field] !== undefined) { + currentFieldValue = deepFields[field]; + } + else { + currentFieldValue = currentElement.value; + } + if (currentFieldValue !== '') { + return exp.replaceAll(`:${field}`, currentFieldValue); + } + return exp; + } + + const handleFieldChange = (e) => { + // If the value was clear. Rollback the element to the initial state. + if (e.target.value === "" && previousValue[e.target.name] !== undefined && previousValue[e.target.name] !== "") { + updateResult(fieldElement, ""); + return; + } + + const convertedExpression = fields.reduce(buildExpression, expression); if (convertedExpression.split(':').length === 1) { - fieldElement.value = math.evaluate(convertedExpression); - fieldElement.dispatchEvent(new Event('change')); + let expressionResult = math.evaluate(convertedExpression); + if (fieldEvaluateRound !== null) { + expressionResult = parseFloat(expressionResult).toFixed(fieldEvaluateRound) + } + updateResult(fieldElement, expressionResult); } + previousValue[e.target.name] = e.target.value; + } + + let selectorFormContext = ''; + if (formContext !== '') { + selectorFormContext = `form#${formContext} `; } - fieldNames.forEach(name => { - const element = document.querySelector(`form#${formContext} input[name="${name}"], select[name="${name}"]`); + fields.forEach(name => { + const elementSelector = `${selectorFormContext}input[name="${name}"], select[name="${name}"]`; + const element = document.querySelector(elementSelector); if (element) { element.removeEventListener('change', handleFieldChange); element.addEventListener('change', handleFieldChange); @@ -38,9 +86,12 @@ }); } - document - .querySelectorAll('.js-calc-field:not(.built)') - .forEach(el => { + const formMathElements = document + .querySelectorAll('.js-form-math-element:not(.built)'); + if (formMathElements.length === 0) { + return; + } + formMathElements.forEach(el => { if ( el.dataset.evaluateFields !== undefined && el.dataset.evaluateExpression !== undefined @@ -52,7 +103,7 @@ console.error(`Failed to calculate ${e}`); } } - }) + }); }, } })(Drupal, math); diff --git a/modules/calculation_fields_example/calculation_fields_example.info.yml b/modules/calculation_fields_example/calculation_fields_example.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..307f65851266e5568481d40da4a922db5e2bf0a9 --- /dev/null +++ b/modules/calculation_fields_example/calculation_fields_example.info.yml @@ -0,0 +1,7 @@ +name: Calculation Fields Example +type: module +description: Provides examples how to use calculation fields in an normal form or webform. +package: Fields +core_version_requirement: ^9 || ^10 +dependencies: + - calculation_fields:calculation_fields diff --git a/modules/calculation_fields_example/calculation_fields_example.module b/modules/calculation_fields_example/calculation_fields_example.module new file mode 100644 index 0000000000000000000000000000000000000000..756acc2a008e73a83cdc77fee656d4b389f6814f --- /dev/null +++ b/modules/calculation_fields_example/calculation_fields_example.module @@ -0,0 +1,20 @@ +<?php + +/** + * @file + * Contains custom feature for the module. + */ + +use Drupal\Core\Url; + +/** + * Implements hook_help(). + */ +function calculation_fields_example_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.calculation_fields_example': + return '<p>' . t('Take a look on the following <a href=":form">form</a> and webforms <a href="/form/webform-calculation-fields-examp">Webform Calculation fields example</a>, <a href="/form/webform-calculation-fields-multi">Webform Calculation fields multistep example</a>.', [ + ':form' => Url::fromRoute('calculation_fields_example.form_api')->toString() + ]) . '</p>'; + } +} diff --git a/modules/calculation_fields_example/calculation_fields_example.routing.yml b/modules/calculation_fields_example/calculation_fields_example.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..b569d1e457a37a31cad8511f3fca734d6dda53ff --- /dev/null +++ b/modules/calculation_fields_example/calculation_fields_example.routing.yml @@ -0,0 +1,7 @@ +calculation_fields_example.form_api: + path: '/calculation-fields-example/form' + defaults: + _title: 'Example' + _form: 'Drupal\calculation_fields_example\Form\ExampleForm' + requirements: + _permission: 'access content' diff --git a/src/Form/ExampleForm.php b/modules/calculation_fields_example/src/Form/ExampleForm.php similarity index 62% rename from src/Form/ExampleForm.php rename to modules/calculation_fields_example/src/Form/ExampleForm.php index 0a53c32f30bf4df8b6ca34e7ac6189bf80de6f1e..d9fcb79abd157c4e9cb1e8964b88bcb8486cca16 100644 --- a/src/Form/ExampleForm.php +++ b/modules/calculation_fields_example/src/Form/ExampleForm.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\calculation_fields\Form; +namespace Drupal\calculation_fields_example\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; @@ -30,7 +30,7 @@ class ExampleForm extends FormBase { $form['second_value'] = [ '#type' => 'number', - '#title' => $this->t('SEcond value'), + '#title' => $this->t('Second value'), '#required' => TRUE, '#min' => 0, '#max' => 2000, @@ -61,7 +61,21 @@ class ExampleForm extends FormBase { '#type' => 'form_element_math', '#title' => $this->t('Result'), '#required' => TRUE, - '#evaluation_fields' => '(:first_value + :second_value + :third_value) * :multiple' + '#evaluation_fields' => '(:first_value + :second_value + :third_value) * :multiple', + '#evaluation_round' => 2, + ]; + + $form['result_markup'] = [ + '#type' => 'form_element_markup_math', + '#evaluation_fields' => '<h3>This is my test result divided by 2: R$ <span>{{ ((:first_value + :second_value + :third_value) * :multiple) / 2 }}</span></h3>', + '#evaluation_text_preview' => $this->t('With preview markup'), + '#evaluation_round' => 2, + ]; + + $form['result_markup_1'] = [ + '#type' => 'form_element_markup_math', + '#evaluation_fields' => '<h3>This is my test result: R$ <span>{{ (:first_value + :second_value + :third_value) * :multiple }}</span></h3>', + '#evaluation_round' => 2, ]; $form['actions'] = [ @@ -75,21 +89,13 @@ class ExampleForm extends FormBase { return $form; } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if (mb_strlen($form_state->getValue('message')) < 10) { - $form_state->setErrorByName('message', $this->t('Message should be at least 10 characters.')); - } - } - /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->messenger()->addStatus($this->t('The message has been sent.')); - $form_state->setRedirect('<front>'); + foreach (['result', 'result_markup', 'result_markup_1'] as $key) { + $this->messenger()->addStatus($this->t('The result submitted was: ' . $form_state->getValue($key))); + } } } diff --git a/modules/webform_calculation_fields/README.md b/modules/webform_calculation_fields/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1bed30b6b4902dbcb461bf70c168f377e6dcf7e2 --- /dev/null +++ b/modules/webform_calculation_fields/README.md @@ -0,0 +1,52 @@ +# Webform Calculation Fields + +## What the Module Does +The "Webform Calculation Fields" module is an extension for Drupal's Webform module, designed to enhance the capabilities of webforms by introducing a custom field type called "Field calculator." This field allows you to perform dynamic calculations within your webforms, similar to the "form_element_math" module, while benefiting from Webform's flexibility and user-friendly interface. + +## How to Use the Custom Calculation Field in Webforms + +### 1. Installing and Enabling the Module +Before using the custom "Field calculator" field, make sure you've installed and enabled the "Webform Calculation Fields" module. You can do this via the Drupal admin interface. + +### 2. Creating a Webform +Create a new webform or edit an existing one, depending on your requirements. + +### 3. Adding a Calculation Field +To add a Calculation field to your webform, follow these steps: + +1. In your webform, navigate to the "Add Element" section. +2. Look for the "Field calculator" element type, which is provided by the "Webform Calculation Fields" module. +3. Click on the "Field calculator" element to add it to your webform. + +### 4. Configuring the Calculation Field +Once you've added the Calculation field to your webform, you'll need to configure it. Here are the essential settings: + +- **Title**: Give the field a descriptive title. +- **Calculation Formula**: In this field, you can specify the mathematical expression you want to use for the calculation. You can use the same format as in the "form_element_math" module, with field names enclosed in colons, such as `:field1 + :field2`. You can reference any other fields in the webform using colons. +- **Decimal Places**: Choose the number of decimal places to round the result to. + +### 5. Permissions +The "Webform Calculation Fields" module provides permissions to control who can create, edit, and delete elements of the "Calculation" type. Make sure to configure these permissions according to your site's needs. + +### 6. Saving and Using the Webform +After configuring the Calculation field, save your webform, and then it's ready to use. When users fill out the webform, the Calculation field will automatically evaluate the specified formula and display the result. + +## Example 1: Simple Calculator +Suppose you're creating a simple webform where users can input two numbers, and you want to display the sum of those numbers. Here's how you can configure the Calculation field: + +- Title: "Result" +- Calculation Formula: `:number1 + :number2` +- Decimal Places: 2 + +In this example, ":number1" and ":number2" should be replaced with the actual field names you've defined in your webform. When users fill in these fields, the "Result" field will automatically calculate and display the sum. + +## Example 2: Loan Repayment Calculator +Imagine you're creating a webform for a loan application, and you want to display the monthly loan repayment amount based on the loan amount, interest rate, and loan term. Here's how you can configure the Calculation field: + +- Title: "Monthly Payment" +- Calculation Formula: `(:loan_amount * :interest_rate * (1 + :interest_rate) ^ :loan_term) / ((1 + :interest_rate) ^ :loan_term - 1)` +- Decimal Places: 2 + +In this example, ":loan_amount," ":interest_rate," and ":loan_term" should be replaced with the actual field names you've defined in your webform. The "Monthly Payment" field will automatically calculate and display the monthly repayment amount based on the user's input. + +With the "Webform Calculation Fields" module, you can easily incorporate dynamic calculations into your webforms, making them more interactive and user-friendly. diff --git a/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/config/install/webform.webform.webform_calculation_fields_examp.yml b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/config/install/webform.webform.webform_calculation_fields_examp.yml new file mode 100644 index 0000000000000000000000000000000000000000..9f47dd0aec0ff33578a035ee7c2dad25a8050cc5 --- /dev/null +++ b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/config/install/webform.webform.webform_calculation_fields_examp.yml @@ -0,0 +1,230 @@ +uuid: ce514239-490a-4df9-b2b1-aa114ffb7714 +langcode: en +status: open +dependencies: { } +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: webform_calculation_fields_examp +title: 'Webform Calculation fields example' +description: '' +categories: { } +elements: |- + first_value: + '#type': number + '#title': 'First value' + second_value: + '#type': number + '#title': 'Second Value' + first_second: + '#type': form_element_math + '#title': 'First + Second' + '#evaluation_fields': ':first_value + :second_value' + multiply: + '#type': select + '#title': Multiply + '#options': + 10: '10' + 100: '100' + 1000: '1000' + multiply_result: + '#type': form_element_math + '#title': 'Multiply - Result' + '#evaluation_fields': ':first_second * :multiply' + to_divide: + '#type': number + '#title': 'To Divide' + '#min': 1 + multiply_result_to_divide: + '#type': form_element_math + '#title': 'Multiply Result / To Divide' + '#evaluation_round': 2 + '#evaluation_fields': ':multiply_result / :to_divide' +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: '' + ajax_effect: '' + ajax_speed: null + page: true + page_submit_path: '' + page_confirm_path: '' + page_theme_name: '' + form_title: both + form_submit_once: false + form_open_message: '' + form_close_message: '' + form_exception_message: '' + form_previous_submissions: true + form_confidential: false + form_confidential_message: '' + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + form_attributes: { } + form_method: '' + form_action: '' + share: false + share_node: false + share_theme_name: '' + share_title: true + share_page_body_attributes: { } + submission_label: '' + submission_exception_message: '' + submission_locked_message: '' + submission_log: false + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '' + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: '' + wizard_prev_button_label: '' + wizard_next_button_label: '' + wizard_toggle: false + wizard_toggle_show_label: '' + wizard_toggle_hide_label: '' + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: '' + preview_title: '' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + draft_pending_single_message: '' + draft_pending_multiple_message: '' + confirmation_type: page + confirmation_url: '' + confirmation_title: '' + confirmation_message: '' + confirmation_attributes: { } + confirmation_back: true + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: none + purge_days: null + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: { } +variants: { } \ No newline at end of file diff --git a/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.info.yml b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..e984c2092ac7d99478b50a5cfbea8f3956bcc598 --- /dev/null +++ b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.info.yml @@ -0,0 +1,9 @@ +name: Webform Calculation Fields Examples +type: module +description: Provides webform examples using calculation fiel types. +package: Webform +core_version_requirement: ^9 || ^10 +dependencies: + - calculation_fields:calculation_fields + - webform_calculation_fields:webform_calculation_fields + - webform:webform diff --git a/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.install b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.install new file mode 100644 index 0000000000000000000000000000000000000000..5171d87355e1edd1dcccca5c23e7b090f0fab078 --- /dev/null +++ b/modules/webform_calculation_fields/modules/webform_calculation_fields_examples/webform_calculation_fields_examples.install @@ -0,0 +1,30 @@ +<?php + +/** + * Implements uninstall_hook(). + */ +function webform_calculation_fields_examples_uninstall() { + $webform_machine_names = [ + 'webform_calculation_fields_examp', + ]; + + + $query = \Drupal::entityQuery('webform_submission') + ->condition('webform_id', $webform_machine_names, 'IN'); + $submission_ids = $query->execute(); + if (!empty($submission_ids)) { + $submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission'); + $submissions = $submission_storage->loadMultiple($submission_ids); + $submission_storage->delete($submissions); + } + + // Delete webforms. + $webform_storage = \Drupal::entityTypeManager()->getStorage('webform'); + foreach ($webform_machine_names as $webform_machine_name) { + $webforms = $webform_storage->loadByProperties(['id' => $webform_machine_name]); + if (!empty($webforms)) { + $webform_storage->delete($webforms); + } + } + +} diff --git a/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsElementUiAccess.php b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsElementUiAccess.php new file mode 100644 index 0000000000000000000000000000000000000000..8d62b1224a66d35636b99b4a7a556f6da9c24c72 --- /dev/null +++ b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsElementUiAccess.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\webform_calculation_fields\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\user\Entity\User; +use Drupal\webform\WebformInterface; + +/** + * Provide access feature for the custom math element. + */ +trait WebformCalculationFieldsElementUiAccess { + + /** + * {@inheritDoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, WebformInterface $webform = NULL, $key = NULL, $parent_key = NULL, $type = NULL) { + $router_match = $this->getRouteMatch(); + $field_key = $router_match->getParameter('key'); + $field_props = $webform->getElement($field_key); + $element_type = $field_props['#webform_plugin_id']; + $uid = $this->currentUser()->id(); + if ($element_type === 'form_element_math' && !User::load($uid)->hasPermission('administer calculation_fields configuration')) { + return [ + "#type" => "markup", + "#markup" => t('You do not have access to delete this field.'), + ]; + } + return parent::buildForm($form, $form_state, $webform, $key, $parent_key, $type); + } + +} diff --git a/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsForm.php b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..ab86fbcb62b3393e0dcb2dbcbc7898bbe9f6cf60 --- /dev/null +++ b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsForm.php @@ -0,0 +1,14 @@ +<?php + +namespace Drupal\webform_calculation_fields\Form; + +use Drupal\webform_ui\Form\WebformUiElementEditForm; + +/** + * Overrides the form element checking the permission before delete the element. + */ +class WebformCalculationFieldsForm extends WebformUiElementEditForm { + + use WebformCalculationFieldsElementUiAccess; + +} diff --git a/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsFormDelete.php b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsFormDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..99985582783f5378390d06262d7453782223a101 --- /dev/null +++ b/modules/webform_calculation_fields/src/Form/WebformCalculationFieldsFormDelete.php @@ -0,0 +1,14 @@ +<?php + +namespace Drupal\webform_calculation_fields\Form; + +use Drupal\webform_ui\Form\WebformUiElementDeleteForm; + +/** + * Overrides the form delete element checking the permission before delete the element. + */ +class WebformCalculationFieldsFormDelete extends WebformUiElementDeleteForm { + + use WebformCalculationFieldsElementUiAccess; + +} diff --git a/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformFieldCalculator.php b/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformFieldCalculator.php index c19c9ec3dd9f41383dca2a9222e0a5f9e899ded5..5dcec7e3ec6379e948ee2fd92066425ddf05ab8f 100644 --- a/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformFieldCalculator.php +++ b/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformFieldCalculator.php @@ -3,28 +3,34 @@ namespace Drupal\webform_calculation_fields\Plugin\WebformElement; use Drupal\Core\Form\FormStateInterface; +use Drupal\user\Entity\User; use Drupal\webform\Plugin\WebformElement\Number; /** - * Provides a 'Webform Remote Fields Hidden' element. + * Provides a Webform Math element. * * @WebformElement( * id = "form_element_math", * label = @Translation("Field calculator"), - * api = "https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!Element!Number.php/class/Number", - * description = @Translation("Provides a hidden form element with API Integration."), + * description = @Translation("Provides a math element capable to evaluate math expressions."), * category = @Translation("Calculator"), * ) */ class WebformFieldCalculator extends Number { - + /** + * {@inheritDoc} + */ public function defineDefaultProperties() { return parent::defineDefaultProperties() + [ 'evaluation_fields' => '', + 'evaluation_round' => NULL, ]; } + /** + * {@inheritDoc} + */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); @@ -34,13 +40,20 @@ class WebformFieldCalculator extends Number { '#weight' => -1, ]; + $form['options']['calculator']['evaluation_round'] = [ + '#type' => 'number', + '#min' => 0, + '#title' => $this->t('Round result'), + '#description' => $this->t('How many decimal cases display'), + '#weight' => 1, + ]; + $form['options']['calculator']['evaluation_fields'] = [ '#type' => 'textarea', '#title' => $this->t('Add here the expression'), '#description' => $this->t('Example of expressions: field_1 + field_2 - (field_1 * field_3)'), - '#weight' => 1, + '#weight' => 2, ]; - return $form; } diff --git a/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformMathMarkupElement.php b/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformMathMarkupElement.php new file mode 100644 index 0000000000000000000000000000000000000000..db3f8ea933b889d32b12b47a449b57ceecb6b2bc --- /dev/null +++ b/modules/webform_calculation_fields/src/Plugin/WebformElement/WebformMathMarkupElement.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\webform_calculation_fields\Plugin\WebformElement; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\webform\Plugin\WebformElement\TextField; + +/** + * Provides a Webform Math element. + * + * @WebformElement( + * id = "form_element_markup_math", + * label = @Translation("Field calculator markup"), + * description = @Translation("Provided an advance markup that can evaluate expression inside {{ }} tags."), + * category = @Translation("Calculator"), + * ) + */ +class WebformMathMarkupElement extends TextField { + + /** + * {@inheritDoc} + */ + public function defineDefaultProperties() { + $default = parent::defineDefaultProperties(); + $custom = [ + 'evaluation_round' => NULL, + 'evaluation_fields' => '', + 'evaluation_text_preview' => '', + ]; + return array_merge( + $default, + $custom + ); + } + + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + unset($form["summary_attributes"]); + unset($form["validation"]); + unset($form["element_description"]); + unset($form["form"]); +// unset($form["element"]); + + + $form['markup']['evaluation_fields'] = [ + '#type' => 'webform_html_editor', + '#title' => $this->t('HTML markup'), + '#description' => $this->t('Enter custom HTML into your webform. Add the expression between {{ }}. Like {{ :field_x + :field_y }}'), + ]; + + $form['markup']['evaluation_round'] = [ + '#type' => 'number', + '#min' => 0, + '#title' => $this->t('Round result'), + '#description' => $this->t('How many decimal cases display'), + '#weight' => 1, + ]; + + $form['markup']['evaluation_text_preview'] = [ + '#type' => 'textfield', + '#title' => $this->t('Add the preview text'), + '#description' => $this->t('Text that be displayed until all inputs used by this field be populated. If not provided the element will displayed when the calculate is finished.'), + '#weight' => 2, + ]; + return $form; + } + +} diff --git a/modules/webform_calculation_fields/src/Routing/WebformCalculationFieldsRouting.php b/modules/webform_calculation_fields/src/Routing/WebformCalculationFieldsRouting.php new file mode 100644 index 0000000000000000000000000000000000000000..a3a9a8ac092028fb40f21afcc8400b5ffba6bb3e --- /dev/null +++ b/modules/webform_calculation_fields/src/Routing/WebformCalculationFieldsRouting.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\webform_calculation_fields\Routing; + +use Drupal\Core\Routing\RouteSubscriberBase; +use Drupal\webform_calculation_fields\Form\WebformCalculationFieldsForm; +use Drupal\webform_calculation_fields\Form\WebformCalculationFieldsFormDelete; +use Symfony\Component\Routing\RouteCollection; + +/** + * Subscribe alter route event. + * + * Update the routes defined adding new form that validate if the user can + * do anything with the form element. + */ +class WebformCalculationFieldsRouting extends RouteSubscriberBase { + + /** + * Routes to set the new form behavior. + * + * @var string[] + */ + protected static $routes = [ + 'entity.webform_ui.element.edit_form' => WebformCalculationFieldsForm::class, + 'entity.webform_ui.element.delete_form' => WebformCalculationFieldsFormDelete::class, + ]; + + /** + * {@inheritDoc} + */ + protected function alterRoutes(RouteCollection $collection) { + foreach (static::$routes as $routeName => $form) { + $routeInfo = $collection->get($routeName); + if ($routeInfo) { + $routeInfo->setDefault('_form', $form); + } + } + } + +} diff --git a/modules/webform_calculation_fields/webform_calculation_fields.info.yml b/modules/webform_calculation_fields/webform_calculation_fields.info.yml index 942c71f047fba21aee02f5bb396663d42361d0b2..f36650fc7ef4f1ec7760d58a5ed09f7c1c9dab05 100644 --- a/modules/webform_calculation_fields/webform_calculation_fields.info.yml +++ b/modules/webform_calculation_fields/webform_calculation_fields.info.yml @@ -1,6 +1,6 @@ name: Webform Calculation Fields type: module -description: Provides additional functionality for the site. +description: Provides additional form math element to realize math expression combining fields. package: Webform core_version_requirement: ^9 || ^10 dependencies: diff --git a/modules/webform_calculation_fields/webform_calculation_fields.services.yml b/modules/webform_calculation_fields/webform_calculation_fields.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..96ec1c1cdb15da750715b60f2a95911963f26e90 --- /dev/null +++ b/modules/webform_calculation_fields/webform_calculation_fields.services.yml @@ -0,0 +1,5 @@ +services: + webform_calculation_fields.router_subscriber: + class: 'Drupal\webform_calculation_fields\Routing\WebformCalculationFieldsRouting' + tags: + - { name: event_subscriber } diff --git a/src/Element/FormMathElement.php b/src/Element/FormMathElement.php index 9967c08244a21f09f4161bdd1f76962002ce6afa..0e3f1a916aa350db07ca94b24a25f8d10a59394a 100644 --- a/src/Element/FormMathElement.php +++ b/src/Element/FormMathElement.php @@ -10,10 +10,16 @@ use Drupal\webform\Utility\WebformElementHelper; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** + * Provides a custom element capable to realize some math expression displaying, + * the result of the expression. + * * @FormElement("form_element_math") */ class FormMathElement extends Number { + /** + * {@inheritDoc} + */ public function getInfo() { $info = parent::getInfo(); $info['#evaluation_fields'] = NULL; @@ -22,8 +28,14 @@ class FormMathElement extends Number { return $info; } + /** + * Validate form math element callback. + * + * To avoid disruptive data from the client, every submit evaluate the + * expression and set the correct/expected value for the element. + */ public static function validate(&$element, FormStateInterface $form_state, &$complete_form) { - $expression = static::mountEvaluation($element["#evaluation_fields"], $form_state); + $expression = static::buildFieldEvaluation($element["#evaluation_fields"], $form_state); $value = NestedArray::getValue($form_state->getValues(), $element['#parents']); if (empty($expression) || empty($value)) { return; @@ -40,18 +52,41 @@ class FormMathElement extends Number { */ public static function preRenderNumber($element) { $element['#attributes']['type'] = 'number'; + $element['#attributes']['readonly'] = 'readonly'; Element::setAttributes($element, ['id', 'name', 'value', 'step', 'min', 'max', 'placeholder', 'size', 'fields']); - static::setAttributes($element, ['form-number', 'js-calc-field']); + static::setAttributes($element, ['form-number', 'js-form-math-element']); $element['#attributes']['data-evaluate-expression'] = $element["#evaluation_fields"]; $element['#attributes']['data-evaluate-fields'] = static::evaluateFields($element["#evaluation_fields"]); $element['#attached']['library'][] = 'calculation_fields/calculation_fields'; return $element; } - public static function processFormMathElement($element, \Drupal\Core\Form\FormStateInterface $form_state, $form) { + /** + * Process each form math element setting custom properties. + * + * Set two properties: + * - data-evaluate-context: used to gurantee that the input will use values, + * in the correct form context. + * - data-evaluate-deep-fields: deep fields properties is used to store + * values of fields that are not being displayed like, like in a multistep + * form in a webform for example. + * + * @param array $element + * The element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state instance. + */ + public static function processFormMathElement(array $element, FormStateInterface $form_state, $form) { $element['#attributes']['data-evaluate-context'] = str_replace('_', '-', $form["#form_id"]); + if (isset($element['#evaluation_round'])) { + $element['#attributes']['data-evaluate-round'] = (int) $element['#evaluation_round']; + } $dependency_fields = static::evaluateFields($element["#evaluation_fields"]); + $has_multiple_steps = $form["progress"]["#current_page"] ?? NULL; + $completed_form = $form_state->getCompleteForm(); + $trigerred = $form_state->getTriggeringElement(); + $values_dependencies = []; foreach ($dependency_fields as $field) { if (!empty($form['elements'])) { @@ -61,7 +96,7 @@ class FormMathElement extends Number { $d_element = WebformElementHelper::getElement($form, $field); } - if (!$d_element['#access']) { + if ($d_element["#webform_parent_key"] !== $has_multiple_steps || !$d_element['#access']) { $values_dependencies[$field] = $form_state->getValue($field); } } @@ -69,7 +104,20 @@ class FormMathElement extends Number { return $element; } - protected static function mountEvaluation($evaluation, FormStateInterface $form_state) { + /** + * Build expression field replacing field name for this value. + * + * Example: (:field_name_1 + :field_name2) became (2 + 2). + * + * @param string $evaluation + * The field expression. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * + * @return string + * The expression with values. + */ + public static function buildFieldEvaluation($evaluation, FormStateInterface $form_state) { $fields = static::evaluateFields($evaluation); $collected_values = []; foreach ($fields as $field) { @@ -88,13 +136,20 @@ class FormMathElement extends Number { return $exp; } - - + /** + * Evaluation fields expression getting the field names. + * + * @param string $evaluation_fields + * Evaluation field expression. + * + * @return array + * An array with the field names. + */ public static function evaluateFields($evaluation_fields): array { - $fields = explode(':', $evaluation_fields) ?? []; - $fields = array_filter($fields); - $patterns = []; - foreach ($fields as $field) { + $expression_fields = explode(':', $evaluation_fields) ?? []; + $expression_fields = array_filter($expression_fields); + $fields = []; + foreach ($expression_fields as $field) { $items = explode(' ', $field); $items = array_map(function ($item) { return preg_replace("/[^a-zA-Z0-9_]/", "", $item); @@ -102,9 +157,9 @@ class FormMathElement extends Number { $filteredItems = array_filter($items, function ($item) { return !is_numeric($item) && !empty($item); }); - $patterns = array_merge($patterns, $filteredItems); + $fields = array_merge($fields, $filteredItems); } - return $patterns; + return $fields; } diff --git a/src/Element/FormMathMarkupElement.php b/src/Element/FormMathMarkupElement.php new file mode 100644 index 0000000000000000000000000000000000000000..928ed6e28ced024314ed837e0c584472597b8a16 --- /dev/null +++ b/src/Element/FormMathMarkupElement.php @@ -0,0 +1,102 @@ +<?php + +namespace Drupal\calculation_fields\Element; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\Textfield; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * Provides a custom element capable to realize some math expression displaying, + * the result of the expression. + * + * @FormElement("form_element_markup_math") + */ +class FormMathMarkupElement extends Textfield { + + /** + * {@inheritDoc} + */ + public function getInfo() { + $info = parent::getInfo(); + $info['#process'][] = self::class . '::processFormMathElement'; + $info['#element_validate'][] = self::class . '::validateExpressionValue'; + return $info; + } + + /** + * Validate form math element callback. + * + * To avoid disruptive data from the client, every submit evaluate the + * expression and set the correct/expected value for the element. + */ + public static function validateExpressionValue(&$element, FormStateInterface $form_state, &$complete_form) { + $expressionFields = static::extractEvaluation($element); + $expressionReplaced = FormMathElement::buildFieldEvaluation($expressionFields, $form_state); + $value = NestedArray::getValue($form_state->getValues(), $element['#parents']); + if (empty($expressionFields) || empty($value)) { + return; + } + $expressionLanguage = new ExpressionLanguage(); + $result = $expressionLanguage->evaluate($expressionReplaced); + if ($element["#evaluation_round"]) { + $result = number_format($result, $element["#evaluation_round"], '.', ''); + } + $expectedResult = str_replace('{{ ' . $expressionFields . ' }}', $result, $element["#evaluation_fields"]); + if ($expectedResult !== $value) { + $form_state->setValue($element['#name'], $expectedResult); + } + } + + /** + * Process function to get the form context of the element. + */ + public static function processFormMathElement(array &$element, FormStateInterface $form_state, $form) { + $element['#attributes']['data-evaluate-context'] = str_replace('_', '-', $form["#form_id"]); + return $element; + } + + /** + * Extract evaluation from the element. + */ + public static function extractEvaluation($element) { + $pattern = '/{{(.*?)}}/'; + preg_match_all($pattern, $element['#evaluation_fields'], $matches); + if (empty($matches[1])) { + \Drupal::logger('calculation_fields') + ->warning(t('Failed to extract expression of the evaluation_fields value @evaluation_fields', [ + '@name' => $element['name'], + '@evaluation_fields' => $element['#evaluation_fields'], + ])); + return [ + '#markup' => '', + ]; + } + return trim(current($matches[1])); + } + + /** + * Pre render markup that add the dataset of the evaluation data. + */ + public static function preRenderTextfield($element) { + $expression = static::extractEvaluation($element); + $formContext = ''; + if (isset($element["#attributes"]["data-evaluate-context"])) { + $formContext = $element["#attributes"]["data-evaluate-context"]; + } + // @todo strip tags. + $textExpression = str_replace('{{ ' . $expression . ' }}', '[RESULT]', $element['#evaluation_fields']); + return [ + '#theme' => 'form_math_markup_element', + '#round' => $element['#evaluation_round'] ?? NULL, + '#formContext' => $formContext, + '#fields' => FormMathElement::evaluateFields($expression), + '#expression' => $expression, + '#previewText' => $element['#evaluation_text_preview'] ?? NULL, + '#textExpression' => $textExpression, + '#name' => $element["#name"] ?? $element["#webform_key"], + ]; + } + +} diff --git a/templates/form-math-markup-element.html.twig b/templates/form-math-markup-element.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..b07b9ad79f00865ab148928604c1df97fcf45162 --- /dev/null +++ b/templates/form-math-markup-element.html.twig @@ -0,0 +1,16 @@ +{{ attach_library('calculation_fields/calculation_fields') }} +<div + data-evaluate-round='{{ round }}' + data-evaluate-context='{{ formContext }}' + data-evaluate-fields='{{ fields|join(' ') }}' + data-evaluate-expression="{{ expression }}" + class="js-form-math-element"> + <div class='form-math-markup-element'> + {% if previewText %} + <p class='js-math-preview'>{{ previewText }}</p> + {% endif %} + <div class='js-math-result'>{{ textExpression|raw }}</div> + <div class='js-math-result-base'>{{ textExpression|raw }}</div> + <input class="js-math-result-input" type="hidden" value="" name="{{ name }}"> + </div> +</div>