PageSourceSearch

https://www.charleskrypell.com/skeleton/1.0/view/js/form_submit_guard.js?v=1787332896

js charleskrypell.com collected 2026-09-27 01:46:18 UTC 3,431 bytes, 96 lines download raw bytes

1/**
2 * Prevents double form submissions across the frontend: the first click on a submit
3 * button disables that form's submit buttons, so a visitor cannot fire the same
4 * request - and the same reCAPTCHA token, which Google rejects on the second use -
5 * by clicking twice. Every click is also logged, see logClick() below.
6 *
7 * Hooked on the button click and not on the form's submit event on purpose: when a
8 * page's own submit handler throws (a missing reCAPTCHA widget does exactly that),
9 * the event never reaches a delegated submit handler, while the click still does.
10 *
11 * The buttons come back after a short window, or right away when a request failed,
12 * so no form is left with a dead button. Forms failing HTML5 validation are not
13 * touched at all - nothing is being sent, so there is nothing to guard.
14 */
15(function ($) {
16    'use strict';
17
18    var LOCK_TIME = 5000; // long enough to swallow repeated clicks on one submission
19    var guarded = [];     // forms locked by this script, newest last
20
21    function unlock(entry) {
22        if (!entry || entry.done) {
23            return;
24        }
25        entry.done = true;
26        clearTimeout(entry.timer);
27        entry.$buttons.prop('disabled', false).removeClass('disabled');
28
29        var index = guarded.indexOf(entry);
30        if (index > -1) {
31            guarded.splice(index, 1);
32        }
33    }
34
35    // sent with fetch, not jQuery, to keep this beacon out of the page's own ajax
36    // handlers and out of the ajaxError hook below
37    function logClick(formId) {
38        var url = window.formSubmitLogUrl;
39        if (!url) {
40            return;
41        }
42
43        try {
44            fetch(url, {
45                method: 'POST',
46                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': window.csrf_token },
47                body: JSON.stringify({ s: { form: formId } }),
48                keepalive: true
49            })['catch'](function () {});
50        } catch (e) {}
51    }
52
53    function lock($form, $button) {
54        var $buttons = $form.find('button[type="submit"], input[type="submit"]').not(':disabled');
55
56        if (!$buttons.length) {
57            return;
58        }
59
60        // HTML5 validation stops the submission itself - leave the buttons alone
61        var form = $form.get(0);
62        if (!$button.is('[formnovalidate]') && form && form.checkValidity && !form.checkValidity()) {
63            return;
64        }
65
66        var entry = { $buttons: $buttons, done: false };
67        guarded.push(entry);
68
69        // disabled on the next tick: a control disabled while the click is still being
70        // handled is dropped from the posted data (and from serializeArray)
71        setTimeout(function () {
72            $buttons.prop('disabled', true).addClass('disabled');
73            entry.timer = setTimeout(function () {
74                unlock(entry);
75            }, LOCK_TIME);
76        }, 0);
77    }
78
79    $(document).on('click', 'button[type="submit"], input[type="submit"]', function () {
80        var $button = $(this);
81        var $form = $button.closest('form');
82
83        logClick($form.attr('id') || '');
84
85        if ($form.length) {
86            lock($form, $button);
87        }
88    });
89
90    // a failed request means nothing was submitted - let the visitor retry at once
91    $(document).ajaxError(function () {
92        for (var i = guarded.length - 1; i >= 0; i--) {
93            unlock(guarded[i]);
94        }
95    });
96})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.