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