7 * Triggers when a value in the form changed.
9 * The event triggers when content is typed or pasted in a text field, before
10 * the change event triggers.
15 (function ($, Drupal, debounce) {
20 * Retrieves the summary for the first element.
23 * The text of the summary.
25 $.fn.drupalGetSummary = function () {
26 var callback = this.data('summaryCallback');
27 return (this[0] && callback) ? $.trim(callback(this[0])) : '';
31 * Sets the summary for all matched elements.
33 * @param {function} callback
34 * Either a function that will be called each time the summary is
35 * retrieved or a string (which is returned each time).
38 * jQuery collection of the current element.
40 * @fires event:summaryUpdated
42 * @listens event:formUpdated
44 $.fn.drupalSetSummary = function (callback) {
47 // To facilitate things, the callback should always be a function. If it's
48 // not, we wrap it into an anonymous function which just returns the value.
49 if (typeof callback !== 'function') {
51 callback = function () { return val; };
55 .data('summaryCallback', callback)
56 // To prevent duplicate events, the handlers are first removed and then
58 .off('formUpdated.summary')
59 .on('formUpdated.summary', function () {
60 self.trigger('summaryUpdated');
62 // The actual summaryUpdated handler doesn't fire when the callback is
63 // changed, so we have to do this manually.
64 .trigger('summaryUpdated');
68 * Prevents consecutive form submissions of identical form values.
70 * Repetitive form submissions that would submit the identical form values
71 * are prevented, unless the form values are different to the previously
74 * This is a simplified re-implementation of a user-agent behavior that
75 * should be natively supported by major web browsers, but at this time, only
76 * Firefox has a built-in protection.
78 * A form value-based approach ensures that the constraint is triggered for
79 * consecutive, identical form submissions only. Compared to that, a form
80 * button-based approach would (1) rely on [visible] buttons to exist where
81 * technically not required and (2) require more complex state management if
82 * there are multiple buttons in a form.
84 * This implementation is based on form-level submit events only and relies
85 * on jQuery's serialize() method to determine submitted form values. As such,
86 * the following limitations exist:
88 * - Event handlers on form buttons that preventDefault() do not receive a
89 * double-submit protection. That is deemed to be fine, since such button
90 * events typically trigger reversible client-side or server-side
91 * operations that are local to the context of a form only.
92 * - Changed values in advanced form controls, such as file inputs, are not
93 * part of the form values being compared between consecutive form submits
94 * (due to limitations of jQuery.serialize()). That is deemed to be
95 * acceptable, because if the user forgot to attach a file, then the size of
96 * HTTP payload will most likely be small enough to be fully passed to the
97 * server endpoint within (milli)seconds. If a user mistakenly attached a
98 * wrong file and is technically versed enough to cancel the form submission
99 * (and HTTP payload) in order to attach a different file, then that
100 * edge-case is not supported here.
102 * Lastly, all forms submitted via HTTP GET are idempotent by definition of
103 * HTTP standards, so excluded in this implementation.
105 * @type {Drupal~behavior}
107 Drupal.behaviors.formSingleSubmit = {
108 attach: function () {
109 function onFormSubmit(e) {
110 var $form = $(e.currentTarget);
111 var formValues = $form.serialize();
112 var previousValues = $form.attr('data-drupal-form-submit-last');
113 if (previousValues === formValues) {
117 $form.attr('data-drupal-form-submit-last', formValues);
121 $('body').once('form-single-submit')
122 .on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
127 * Sends a 'formUpdated' event each time a form element is modified.
129 * @param {HTMLElement} element
130 * The element to trigger a form updated event on.
132 * @fires event:formUpdated
134 function triggerFormUpdated(element) {
135 $(element).trigger('formUpdated');
139 * Collects the IDs of all form fields in the given form.
141 * @param {HTMLFormElement} form
142 * The form element to search.
145 * Array of IDs for form fields.
147 function fieldsList(form) {
148 var $fieldList = $(form).find('[name]').map(function (index, element) {
149 // We use id to avoid name duplicates on radio fields and filter out
150 // elements with a name but no id.
151 return element.getAttribute('id');
153 // Return a true array.
154 return $.makeArray($fieldList);
158 * Triggers the 'formUpdated' event on form elements when they are modified.
160 * @type {Drupal~behavior}
162 * @prop {Drupal~behaviorAttach} attach
163 * Attaches formUpdated behaviors.
164 * @prop {Drupal~behaviorDetach} detach
165 * Detaches formUpdated behaviors.
167 * @fires event:formUpdated
169 Drupal.behaviors.formUpdated = {
170 attach: function (context) {
171 var $context = $(context);
172 var contextIsForm = $context.is('form');
173 var $forms = (contextIsForm ? $context : $context.find('form')).once('form-updated');
177 // Initialize form behaviors, use $.makeArray to be able to use native
178 // forEach array method and have the callback parameters in the right
180 $.makeArray($forms).forEach(function (form) {
181 var events = 'change.formUpdated input.formUpdated ';
182 var eventHandler = debounce(function (event) { triggerFormUpdated(event.target); }, 300);
183 formFields = fieldsList(form).join(',');
185 form.setAttribute('data-drupal-form-fields', formFields);
186 $(form).on(events, eventHandler);
189 // On ajax requests context is the form element.
191 formFields = fieldsList(context).join(',');
192 // @todo replace with form.getAttribute() when #1979468 is in.
193 var currentFields = $(context).attr('data-drupal-form-fields');
194 // If there has been a change in the fields or their order, trigger
196 if (formFields !== currentFields) {
197 triggerFormUpdated(context);
202 detach: function (context, settings, trigger) {
203 var $context = $(context);
204 var contextIsForm = $context.is('form');
205 if (trigger === 'unload') {
206 var $forms = (contextIsForm ? $context : $context.find('form')).removeOnce('form-updated');
208 $.makeArray($forms).forEach(function (form) {
209 form.removeAttribute('data-drupal-form-fields');
210 $(form).off('.formUpdated');
218 * Prepopulate form fields with information from the visitor browser.
220 * @type {Drupal~behavior}
222 * @prop {Drupal~behaviorAttach} attach
223 * Attaches the behavior for filling user info from browser.
225 Drupal.behaviors.fillUserInfoFromBrowser = {
226 attach: function (context, settings) {
227 var userInfo = ['name', 'mail', 'homepage'];
228 var $forms = $('[data-user-info-from-browser]').once('user-info-from-browser');
230 userInfo.map(function (info) {
231 var $element = $forms.find('[name=' + info + ']');
232 var browserData = localStorage.getItem('Drupal.visitor.' + info);
233 var emptyOrDefault = ($element.val() === '' || ($element.attr('data-drupal-default-value') === $element.val()));
234 if ($element.length && emptyOrDefault && browserData) {
235 $element.val(browserData);
239 $forms.on('submit', function () {
240 userInfo.map(function (info) {
241 var $element = $forms.find('[name=' + info + ']');
242 if ($element.length) {
243 localStorage.setItem('Drupal.visitor.' + info, $element.val());
250 })(jQuery, Drupal, Drupal.debounce);