Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sylus/00d00f202af23c35749e9178ffbe8820 to your computer and use it in GitHub Desktop.
Save sylus/00d00f202af23c35749e9178ffbe8820 to your computer and use it in GitHub Desktop.
Double click prevention on form submission
From 2a8962fa547e1dbc25768ccabfc8ae01f64998a4 Mon Sep 17 00:00:00 2001
From: sylus <[email protected]>
Date: Wed, 11 Aug 2021 13:38:37 -0400
Subject: [PATCH] Double click prevention on form submission
---
includes/form.inc | 24 ++++++++++++++
misc/form.js | 62 ++++++++++++++++++++++++++++++++++++
modules/system/system.module | 6 ++--
3 files changed, 89 insertions(+), 3 deletions(-)
diff --git a/includes/form.inc b/includes/form.inc
index fa45f9e..1c86f20 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -3380,6 +3380,30 @@ function form_process_actions($element, &$form_state) {
return $element;
}
+/**
+ * Processes a form button element.
+ *
+ * @param $element
+ * An associative array containing the properties and children of the
+ * form button.
+ * @param $form_state
+ * The $form_state array for the form this element belongs to.
+ *
+ * @return
+ * The processed element.
+ */
+function form_process_button($element, &$form_state) {
+ // We normally want to add drupal.form so that the double submit protection
+ // can be added to the site, however, with the addition of
+ // javascript_always_use_jquery, this would make most pages with a login
+ // block or a search form have jquery always added, changing what people who
+ // enabled javascript_always_use_jquery would have expected.
+ if (variable_get('javascript_always_use_jquery', TRUE)) {
+ $element['#attached']['library'][] = array('system', 'drupal.form');
+ }
+ return $element;
+}
+
/**
* Processes a container element.
*
diff --git a/misc/form.js b/misc/form.js
index 259b84e..f7c4c15 100644
--- a/misc/form.js
+++ b/misc/form.js
@@ -57,6 +57,68 @@ Drupal.behaviors.formUpdated = {
}
};
+/**
+ * Prevents consecutive form submissions of identical form values.
+ *
+ * Repetitive form submissions that would submit the identical form values are
+ * prevented, unless the form values are different from the previously
+ * submitted values.
+ *
+ * This is a simplified re-implementation of a user-agent behavior that should
+ * be natively supported by major web browsers, but at this time, only Firefox
+ * has a built-in protection.
+ *
+ * A form value-based approach ensures that the constraint is triggered for
+ * consecutive, identical form submissions only. Compared to that, a form
+ * button-based approach would (1) rely on [visible] buttons to exist where
+ * technically not required and (2) require more complex state management if
+ * there are multiple buttons in a form.
+ *
+ * This implementation is based on form-level submit events only and relies on
+ * jQuery's serialize() method to determine submitted form values. As such, the
+ * following limitations exist:
+ *
+ * - Event handlers on form buttons that preventDefault() do not receive a
+ * double-submit protection. That is deemed to be fine, since such button
+ * events typically trigger reversible client-side or server-side operations
+ * that are local to the context of a form only.
+ * - Changed values in advanced form controls, such as file inputs, are not part
+ * of the form values being compared between consecutive form submits (due to
+ * limitations of jQuery.serialize()). That is deemed to be acceptable,
+ * because if the user forgot to attach a file, then the size of HTTP payload
+ * will most likely be small enough to be fully passed to the server endpoint
+ * within (milli)seconds. If a user mistakenly attached a wrong file and is
+ * technically versed enough to cancel the form submission (and HTTP payload)
+ * in order to attach a different file, then that edge-case is not supported
+ * here.
+ *
+ * Lastly, all forms submitted via HTTP GET are idempotent by definition of HTTP
+ * standards, so excluded in this implementation.
+ */
+Drupal.behaviors.formSingleSubmit = {
+ attach: function () {
+ function onFormSubmit (e) {
+ if (e.isDefaultPrevented()) {
+ // Don't act on form submissions that have been prevented by other JS.
+ return;
+ }
+ var $form = $(e.currentTarget);
+ var formValues = $form.serialize();
+ var previousValues = $form.attr('data-drupal-form-submit-last');
+ if (previousValues === formValues) {
+ e.preventDefault();
+ }
+ else {
+ $form.attr('data-drupal-form-submit-last', formValues);
+ }
+ }
+
+ $('body').once('form-single-submit')
+ .delegate('form:not([method~="GET"])', 'submit.singleSubmit', onFormSubmit);
+
+ }
+};
+
/**
* Prepopulate form fields with information from the visitor cookie.
*/
diff --git a/modules/system/system.module b/modules/system/system.module
index 02785ef..d07ffdc 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -335,7 +335,7 @@ function system_element_info() {
'#button_type' => 'submit',
'#executes_submit_callback' => TRUE,
'#limit_validation_errors' => FALSE,
- '#process' => array('ajax_process_form'),
+ '#process' => array('ajax_process_form', 'form_process_button'),
'#theme_wrappers' => array('button'),
);
$types['button'] = array(
@@ -344,7 +344,7 @@ function system_element_info() {
'#button_type' => 'submit',
'#executes_submit_callback' => FALSE,
'#limit_validation_errors' => FALSE,
- '#process' => array('ajax_process_form'),
+ '#process' => array('ajax_process_form', 'form_process_button'),
'#theme_wrappers' => array('button'),
);
$types['image_button'] = array(
@@ -352,7 +352,7 @@ function system_element_info() {
'#button_type' => 'submit',
'#executes_submit_callback' => TRUE,
'#limit_validation_errors' => FALSE,
- '#process' => array('ajax_process_form'),
+ '#process' => array('ajax_process_form', 'form_process_button'),
'#return_value' => TRUE,
'#has_garbage_value' => TRUE,
'#src' => NULL,
--
2.32.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment