1/** 2 * @file 3 * Form features. 4 */ 5 6/** 7 * Triggers when a value in the form changed. 8 * 9 * The event triggers when content is typed or pasted in a text field, before 10 * the change event triggers. 11 * 12 * @event formUpdated 13 */ 14 15/** 16 * Triggers when a click on a page fragment link or hash change is detected. 17 * 18 * The event triggers when the fragment in the URL changes (a hash change) and 19 * when a link containing a fragment identifier is clicked. In case the hash 20 * changes due to a click this event will only be triggered once. 21 * 22 * @event formFragmentLinkClickOrHashChange 23 */ 24 25(function ($, Drupal, debounce) { 26 /** 27 * Retrieves the summary for the first element. 28 * 29 * @return {string} 30 * The text of the summary. 31 */ 32 $.fn.drupalGetSummary = function () { 33 const callback = this.data('summaryCallback'); 34 35 if (!this[0] || !callback) { 36 return ''; 37 } 38 39 const result = callback(this[0]); 40 41 return result ? result.trim() : ''; 42 }; 43 44 /** 45 * Sets the summary for all matched elements. 46 * 47 * @param {function} callback 48 * Either a function that will be called each time the summary is 49 * retrieved or a string (which is returned each time). 50 * 51 * @return {jQuery} 52 * jQuery collection of the current element. 53 * 54 * @fires event:summaryUpdated 55 * 56 * @listens event:formUpdated 57 */ 58 $.fn.drupalSetSummary = function (callback) { 59 const self = this; 60 61 // To facilitate things, the callback should always be a function. If it's 62 // not, we wrap it into an anonymous function which just returns the value. 63 if (typeof callback !== 'function') { 64 const val = callback; 65 callback = function () { 66 return val; 67 }; 68 } 69 70 return ( 71 this.data('summaryCallback', callback) 72 // To prevent duplicate events, the handlers are first removed and then 73 // (re-)added. 74 .off('formUpdated.summary') 75 .on('formUpdated.summary', () => { 76 self.trigger('summaryUpdated'); 77 }) 78 // The actual summaryUpdated handler doesn't fire when the callback is 79 // changed, so we have to do this manually. 80 .trigger('summaryUpdated') 81 ); 82 }; 83 84 /** 85 * Prevents consecutive form submissions of identical form values. 86 * 87 * Repetitive form submissions that would submit the identical form values 88 * are prevented, unless the form values are different to the previously 89 * submitted values. 90 * 91 * This is a simplified re-implementation of a user-agent behavior that 92 * should be natively supported by major web browsers, but at this time, only 93 * Firefox has a built-in protection. 94 * 95 * A form value-based approach ensures that the constraint is triggered for 96 * consecutive, identical form submissions only. Compared to that, a form 97 * button-based approach would (1) rely on [visible] buttons to exist where 98 * technically not required and (2) require more complex state management if 99 * there are multiple buttons in a form. 100 * 101 * This implementation is based on form-level submit events only and relies 102 * on jQuery's serialize() method to determine submitted form values. As such, 103 * the following limitations exist: 104 * 105 * - Event handlers on form buttons that preventDefault() do not receive a 106 * double-submit protection. That is deemed to be fine, since such button 107 * events typically trigger reversible client-side or server-side 108 * operations that are local to the context of a form only. 109 * - Changed values in advanced form controls, such as file inputs, are not 110 * part of the form values being compared between consecutive form submits 111 * (due to limitations of jQuery.serialize()). That is deemed to be 112 * acceptable, because if the user forgot to attach a file, then the size of 113 * HTTP payload will most likely be small enough to be fully passed to the 114 * server endpoint within seconds, or even milliseconds. If a user 115 * mistakenly attached a wrong file and is technically versed enough to 116 * cancel the form submission (and HTTP payload) in order to attach a 117 * different file, then that edge-case is not supported here. 118 * 119 * Lastly, all forms submitted via HTTP GET are idempotent by definition of 120 * HTTP standards, so excluded in this implementation. 121 * 122 * @type {Drupal~behavior} 123 */ 124 Drupal.behaviors.formSingleSubmit = { 125 attach() { 126 function onFormSubmit(e) { 127 const $form = $(e.currentTarget); 128 const formValues = $form.serialize(); 129 const previousValues = $form.attr('data-drupal-form-submit-last'); 130 if (previousValues === formValues) { 131 e.preventDefault(); 132 } else { 133 $form.attr('data-drupal-form-submit-last', formValues); 134 } 135 } 136 137 $(once('form-single-submit', 'body')).on( 138 'submit.singleSubmit', 139 'form:not([method~="GET"])', 140 onFormSubmit, 141 ); 142 }, 143 }; 144 145 /** 146 * Sends a 'formUpdated' event each time a form element is modified. 147 * 148 * @param {HTMLElement} element 149 * The element to trigger a form updated event on. 150 * 151 * @fires event:formUpdated 152 */ 153 function triggerFormUpdated(element) { 154 $(element).trigger('formUpdated'); 155 } 156 157 /** 158 * Collects the IDs of all form fields in the given form. 159 * 160 * @param {HTMLFormElement} form 161 * The form element to search. 162 * 163 * @return {Array} 164 * Array of IDs for form fields. 165 */ 166 function fieldsList(form) { 167 // We use id to avoid name duplicates on radio fields and filter out 168 // elements with a name but no id. 169 return [].map.call(form.querySelectorAll('[name][id]'), (el) => el.id); 170 } 171 172 /** 173 * Triggers the 'formUpdated' event on form elements when they are modified. 174 * 175 * @type {Drupal~behavior} 176 * 177 * @prop {Drupal~behaviorAttach} attach 178 * Attaches formUpdated behaviors. 179 * @prop {Drupal~behaviorDetach} detach 180 * Detaches formUpdated behaviors. 181 * 182 * @fires event:formUpdated 183 */ 184 Drupal.behaviors.formUpdated = { 185 attach(context) { 186 const $context = $(context); 187 const contextIsForm = context.tagName === 'FORM'; 188 const $forms = $( 189 once('form-updated', contextIsForm ? $context : $context.find('form')), 190 ); 191 let formFields; 192 193 if ($forms.length) { 194 // Initialize form behaviors, use $.makeArray to be able to use native 195 // forEach array method and have the callback parameters in the right 196 // order.
197 $.makeArray($forms).forEach((form) => { 198 const events = 'change.formUpdated input.formUpdated '; 199 const eventHandler = debounce((event) => { 200 triggerFormUpdated(event.target); 201 }, 300); 202 formFields = fieldsList(form).join(','); 203 204 form.setAttribute('data-drupal-form-fields', formFields); 205 $(form).on(events, eventHandler); 206 }); 207 } 208 // On ajax requests context is the form element. 209 if (contextIsForm) { 210 formFields = fieldsList(context).join(','); 211 // @todo replace with form.getAttribute() when #1979468 is in. 212 const currentFields = $(context).attr('data-drupal-form-fields'); 213 // If there has been a change in the fields or their order, trigger 214 // formUpdated. 215 if (formFields !== currentFields) { 216 triggerFormUpdated(context); 217 } 218 } 219 }, 220 detach(context, settings, trigger) { 221 const $context = $(context); 222 const contextIsForm = context.tagName === 'FORM'; 223 if (trigger === 'unload') { 224 once 225 .remove( 226 'form-updated', 227 contextIsForm ? $context : $context.find('form'), 228 ) 229 .forEach((form) => { 230 form.removeAttribute('data-drupal-form-fields'); 231 $(form).off('.formUpdated'); 232 }); 233 } 234 }, 235 }; 236 237 /** 238 * Prepopulate form fields with information from the visitor browser. 239 * 240 * @type {Drupal~behavior} 241 * 242 * @prop {Drupal~behaviorAttach} attach 243 * Attaches the behavior for filling user info from browser. 244 */ 245 Drupal.behaviors.fillUserInfoFromBrowser = { 246 attach(context, settings) { 247 const userInfo = ['name', 'mail', 'homepage']; 248 const $forms = $( 249 once('user-info-from-browser', '[data-user-info-from-browser]'), 250 ); 251 if ($forms.length) { 252 userInfo.forEach((info) => { 253 const $element = $forms.find(`[name=${info}]`); 254 const browserData = localStorage.getItem(`Drupal.visitor.${info}`); 255 if (!$element.length) { 256 return; 257 } 258 const emptyValue = $element[0].value === ''; 259 const defaultValue = 260 $element.attr('data-drupal-default-value') === $element[0].value; 261 if (browserData && (emptyValue || defaultValue)) { 262 $element.each(function (index, item) { 263 item.value = browserData; 264 }); 265 } 266 }); 267 } 268 $forms.on('submit', () => { 269 userInfo.forEach((info) => { 270 const $element = $forms.find(`[name=${info}]`); 271 if ($element.length) { 272 localStorage.setItem(`Drupal.visitor.${info}`, $element[0].value); 273 } 274 }); 275 }); 276 }, 277 }; 278 279 /** 280 * Sends a fragment interaction event on a hash change or fragment link click. 281 * 282 * @param {jQuery.Event} e 283 * The event triggered. 284 * 285 * @fires event:formFragmentLinkClickOrHashChange 286 */ 287 const handleFragmentLinkClickOrHashChange = (e) => { 288 let url; 289 if (e.type === 'click') { 290 url = e.currentTarget.location 291 ? e.currentTarget.location 292 : e.currentTarget; 293 } else { 294 url = window.location; 295 } 296 const hash = url.hash.substring(1); 297 if (hash) { 298 const $target = $(`#${hash}`); 299 $('body').trigger('formFragmentLinkClickOrHashChange', [$target]); 300 301 /** 302 * Clicking a fragment link or a hash change should focus the target 303 * element, but event timing issues in multiple browsers require a timeout. 304 */ 305 setTimeout(() => $target.trigger('focus'), 300); 306 } 307 }; 308 309 const debouncedHandleFragmentLinkClickOrHashChange = debounce( 310 handleFragmentLinkClickOrHashChange, 311 300, 312 true, 313 ); 314 315 // Binds a listener to handle URL fragment changes. 316 $(window).on( 317 'hashchange.form-fragment', 318 debouncedHandleFragmentLinkClickOrHashChange, 319 ); 320 321 /** 322 * Binds a listener to handle clicks on fragment links and absolute URL links 323 * containing a fragment, this is needed next to the hash change listener 324 * because clicking such links doesn't trigger a hash change when the fragment 325 * is already in the URL. 326 */ 327 $(document).on( 328 'click.form-fragment', 329 'a[href*="#"]', 330 debouncedHandleFragmentLinkClickOrHashChange, 331 ); 332})(jQuery, Drupal, Drupal.debounce);
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.