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