fonction « shake » publique.
[Plinn.git] / Products / Plinn / skins / ajax_scripts / ajax_form_manager.js
1 // (c) Benoît PIN 2006-2014
2 // http://plinn.org
3 // Licence GPL
4 //
5 //
6
7 var FormManager;
8 var shake;
9
10 (function(){
11
12 FormManager = function(form, responseTextDest, lazy, noHistory) {
13 if (form.elements.namedItem("noAjax")) {return;}
14
15 this.form = form;
16 this.responseTextDest = responseTextDest;
17 this.lazy = lazy;
18 this.noHistory = noHistory;
19 var thisManager = this;
20 this.form.onsubmit = function(evt) { thisManager.submit(evt); };
21 this.form.onclick = function(evt) { thisManager.click(evt); };
22
23 /* raised on form submit */
24 this.onBeforeSubmit = null;
25 /* raised after xmlhttp response */
26 this.onResponseLoad = null;
27 /* raised when the responseText is added inside the main element.
28 * (onResponseLoad may have the default value) */
29 this.onAfterPopulate = null;
30 this.submitButton = null;
31
32 if (this.lazy) {
33 this.form.onclick = function(evt){
34 thisManager.replaceElementByField(evt);
35 thisManager.click(evt);
36 };
37 if (browser.isDOM2Event) {
38 this.form.onfocus = this.form.onclick;
39 }
40 else if (browser.isIE6up) {
41 this.form.onfocusin = this.form.onclick;
42 }
43 this.onResponseLoad = function(req){ thisManager.restoreField(req); };
44 this.lazyListeners = [];
45 }
46 };
47
48 FormManager.prototype.submit = function(evt) {
49 var form = this.form;
50 var thisManager = this;
51
52 var bsMessage; // before submit message
53 if (!this.onBeforeSubmit) {
54 var onBeforeSubmit = form.elements.namedItem("onBeforeSubmit");
55 if (onBeforeSubmit) {
56 if (onBeforeSubmit.length) {
57 onBeforeSubmit = onBeforeSubmit[0];
58 }
59 /*jslint evil: true */
60 this.onBeforeSubmit = eval(onBeforeSubmit.value);
61 bsMessage = this.onBeforeSubmit(thisManager, evt);
62 }
63 }
64 else {
65 bsMessage = this.onBeforeSubmit(thisManager, evt);
66 }
67
68 if (bsMessage === 'cancelSubmit') {
69 try {disableDefault(evt);}
70 catch (e){}
71 return;
72 }
73
74 if (!this.onResponseLoad) {
75 var onResponseLoad = form.elements.namedItem("onResponseLoad");
76 if (onResponseLoad) {
77 this.onResponseLoad = eval(onResponseLoad.value);
78 }
79 else {
80 this.onResponseLoad = this.loadResponse;
81 }
82 }
83
84 var submitButton = this.submitButton;
85 var queryInfo = this.formData2QueryString();
86 var query = queryInfo.query;
87 this.hasFile = queryInfo.hasFile;
88
89
90 if (!this.onAfterPopulate) {
91 var onAfterPopulate = form.elements.namedItem("onAfterPopulate");
92 if (onAfterPopulate) {
93 this.onAfterPopulate = onAfterPopulate.value;
94 }
95 else {
96 this.onAfterPopulate = function() {};
97 }
98 }
99
100 if (submitButton) {
101 query += submitButton.name + '=' + submitButton.value + '&';
102 }
103
104 if (form.method.toLowerCase() === 'post') {
105 this._post(query);
106 }
107 else {
108 this._get(query);
109 }
110
111 try {disableDefault(evt);}
112 catch (e2){}
113 };
114
115 FormManager.prototype._post = function(query) {
116 // send form by XmlHttpRequest
117 query += "ajax=1";
118
119 var req = new XMLHttpRequest();
120 var thisManager = this;
121 req.onreadystatechange = function() {
122 switch (req.readyState) {
123 case 1 :
124 showProgressImage();
125 break;
126 case 4 :
127 hideProgressImage();
128 if (req.status === 200 || req.status === 204) {
129 thisManager.onResponseLoad(req);
130 }
131 else {
132 alert('Error: ' + req.status);
133 }
134 break;
135 }
136 };
137 var url = this.form.action;
138 req.open("POST", url, true);
139 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
140 req.send(query);
141 };
142
143 FormManager.prototype._get = function(query) {
144 var url = this.form.action;
145 url += '?' + query;
146 AjaxLinkHandler.prototype.loadUrl(url);
147 };
148
149
150 FormManager.prototype.click = function(evt) {
151 var target = getTargetedObject(evt);
152 if(target.type === "submit" || target.type === "image") {
153 this.submitButton = target;
154 disablePropagation(evt);
155 }
156 };
157
158 FormManager.prototype.replaceElementByField = function(evt) {
159 evt = getEventObject(evt);
160 var ob = getTargetedObject(evt);
161 var eventType = evt.type;
162 if (eventType === 'focus' || eventType === 'focusin') {
163 if (this.liveFormField && ob.tagName !== 'INPUT') {
164 this.pendingEvent = [ob, 'click'];
165 }
166 return;
167 }
168 var fieldName = ob.getAttribute('id');
169 if (fieldName) {
170 this.fieldTagName = ob.tagName;
171 var tabIndex = ob.tabIndex;
172 var text;
173 if (ob.firstChild && ob.firstChild.className === 'hidden_value') {
174 text = ob.firstChild.innerHTML;
175 }
176 else {
177 text = ob.innerHTML;
178 }
179 disablePropagation(evt);
180 var parent;
181 thisManager = this;
182 switch (ob.tagName) {
183 case 'SPAN' :
184 // create input element
185 var inputText = document.createElement("input");
186 inputText.setAttribute("type", "text");
187 text = text.replace(/\n/g, ' ');
188 text = text.replace(/\s+/g, ' ');
189 text = text.replace(/^ /, '');
190 text = text.replace(/ $/, '');
191 inputText.setAttribute("value", text);
192 var inputWidth = text.length / 1.9;
193 inputWidth = (inputWidth > 5) ? inputWidth : 5;
194 inputText.style.width = inputWidth + 'em';
195 //inputText.setAttribute("size", text.length);
196
197 // replacement
198 parent = ob.parentNode;
199 parent.replaceChild(inputText, ob);
200
201 inputText.focus();
202 inputText.select();
203 inputText.setAttribute('name', fieldName);
204 inputText.tabIndex = tabIndex;
205 inputText.className = 'live_field';
206 this.liveFormField = inputText;
207 this.lazyListeners.push({'element': inputText, 'eventName' : 'blur', 'handler': function(){ thisManager.submit();}});
208 this.lazyListeners.push({'element': inputText, 'eventName' : 'keypress', 'handler': function(evt){ thisManager._fitField(evt);}});
209 this._addLazyListeners();
210 break;
211
212 case 'DIV' :
213 case 'P' :
214 // create textarea
215 var ta = document.createElement('textarea');
216 ta.style.display = 'block';
217 ta.className = 'live_field';
218 text = text.replace(/^\s*/, '');
219 text = text.replace(/\s*$/, '');
220 ta.value = text;
221
222 // replacement
223 parent = ob.parentNode;
224 parent.replaceChild(ta, ob);
225
226 ta.focus();
227 ta.select();
228 ta.setAttribute('name', fieldName);
229 ta.tabIndex = tabIndex;
230 this.liveFormField = ta;
231 this.lazyListeners.push({'element': ta, 'eventName' : 'blur', 'handler': function(){ thisManager.submit();}});
232 this._addLazyListeners();
233 break;
234 }
235 }
236 };
237
238 FormManager.prototype._addLazyListeners = function() {
239 var i, handlerInfo;
240 for (i=0 ; i<this.lazyListeners.length ; i++) {
241 handlerInfo = this.lazyListeners[i];
242 addListener(handlerInfo.element, handlerInfo.eventName, handlerInfo.handler);
243 }
244 };
245
246 FormManager.prototype._removeLazyListeners = function() {
247 var i, handlerInfo;
248 for (i=0 ; i<this.lazyListeners.length ; i++) {
249 handlerInfo = this.lazyListeners[i];
250 removeListener(handlerInfo.element, handlerInfo.eventName, handlerInfo.handler);
251 }
252 };
253
254
255 FormManager.prototype.restoreField = function(req) {
256 var text;
257 var input = this.liveFormField;
258 if (req.status === 200) {
259 if (req.getResponseHeader('Content-Type').indexOf('text/xml') !== -1) {
260 var out = '..........';
261 if (req.responseXML.documentElement.firstChild) {
262 out = req.responseXML.documentElement.firstChild.nodeValue;
263 }
264
265 switch (req.responseXML.documentElement.nodeName) {
266 case 'computedField':
267 text = out;
268 break;
269 case 'error':
270 this._removeLazyListeners();
271 alert(out);
272 this.pendingEvent = null;
273 input.focus();
274 this._addLazyListeners();
275 return false;
276 }
277 }
278 else {
279 text = req.responseText;
280 }
281 }
282 else {
283 text = '';
284 }
285
286 if (!text.match(/\w/)) {
287 text = '..........';
288 }
289
290 var field = document.createElement(this.fieldTagName);
291 field.innerHTML = text;
292 field.setAttribute('id', input.getAttribute('name'));
293 field.className = 'editable';
294 field.tabIndex = input.tabIndex;
295
296 var parent = input.parentNode;
297 parent.replaceChild(field, input);
298 this.liveFormField = null;
299
300 if (this.pendingEvent) {
301 raiseMouseEvent(this.pendingEvent[0], this.pendingEvent[1]);
302 }
303 return true;
304 };
305
306
307 FormManager.prototype.formData2QueryString = function() {
308 // http://www.onlamp.com/pub/a/onlamp/2005/05/19/xmlhttprequest.html
309 var form = this.form;
310 var strSubmit = '', formElem, elements;
311 var hasFile = false;
312 var i;
313
314 if (!this.lazy) {
315 elements = form.elements;
316 }
317 else {
318 elements = [];
319 var formElements = form.elements;
320 for (i = 0; i < formElements.length; i++) {
321 formElem = formElements[i];
322 switch (formElem.type) {
323 case 'hidden':
324 elements.push(formElem);
325 break;
326 default :
327 if (formElem === this.liveFormField) {
328 elements.push(formElem);
329 }
330 }
331 }
332 }
333
334 for (i = 0; i < elements.length; i++) {
335 formElem = elements[i];
336 switch (formElem.type) {
337 // text, select, hidden, password, textarea elements
338 case 'text':
339 case 'select-one':
340 case 'hidden':
341 case 'password':
342 case 'textarea':
343 strSubmit += formElem.name + '=' + encodeURIComponent(formElem.value) + '&';
344 break;
345 case 'radio':
346 case 'checkbox':
347 if (formElem.checked) {
348 strSubmit += formElem.name + '=' + encodeURIComponent(formElem.value) + '&';
349 }
350 break;
351 case 'select-multiple':
352 var options = formElem.getElementsByTagName("OPTION"), option;
353 var j;
354 for (j = 0 ; j < options.length ; j++) {
355 option = options[j];
356 if (option.selected) {
357 strSubmit += formElem.name + '=' + encodeURIComponent(option.value) + '&';
358 }
359 }
360 break;
361 case 'file':
362 if (formElem.value) {
363 hasFile = true;
364 }
365 break;
366 }
367 }
368 return {'query' : strSubmit, 'hasFile' : hasFile};
369 };
370
371 FormManager.prototype.loadResponse = function(req) {
372 var scripts;
373 if (req.getResponseHeader('Content-Type').indexOf('text/xml') !== -1) {
374 switch(req.responseXML.documentElement.nodeName) {
375 case 'fragments' :
376 if (this.hasFile) {
377 var sb = this.submitButton;
378 if (sb) {
379 var h = document.createElement('input');
380 h.type = 'hidden';
381 h.name = sb.name;
382 h.value = sb.value;
383 this.form.appendChild(h);
384 }
385
386 this.form.submit();
387 return;
388 }
389 var fragments = req.responseXML.documentElement.childNodes;
390 var element, dest, i, j;
391 for (i=0 ; i < fragments.length ; i++) {
392 element = fragments[i];
393 switch (element.nodeName) {
394 case 'fragment' :
395 dest = document.getElementById(element.getAttribute('id'));
396 if(dest) {
397 dest.innerHTML = element.firstChild.nodeValue;
398 scripts = dest.getElementsByTagName('script');
399 for (j=0 ; j < scripts.length ; j++) {
400 globalScriptRegistry.loadScript(scripts[j]); }
401 }
402 break;
403 case 'base' :
404 var headBase = document.getElementsByTagName('base');
405 if (headBase.length > 0) {
406 headBase[0].setAttribute('href', element.getAttribute('href'));
407 }
408 else {
409 headBase = document.createElement('base');
410 headBase.setAttribute('href', element.getAttribute('href'));
411 document.head.appendChild(headBase);
412 }
413 break;
414 }
415 }
416 break;
417 case 'error':
418 alert(req.responseXML.documentElement.firstChild.nodeValue);
419 return;
420 }
421 }
422 else {
423 this.responseTextDest.innerHTML = req.responseText;
424 scripts = this.responseTextDest.getElementsByTagName('script');
425 var k;
426 for (k=0 ; k < scripts.length ; k++) {
427 globalScriptRegistry.loadScript(scripts[k]);
428 }
429 }
430
431 var onAfterPopulate = this.onAfterPopulate;
432 onAfterPopulate();
433 this.scrollToPortalMessage();
434 var url = this.form.action;
435 if (!this.noHistory){ history.pushState(url, document.title, url); }
436 };
437
438 FormManager.prototype.scrollToPortalMessage = function() {
439 var psm = document.getElementById('DesktopStatusBar');
440 if (psm) {
441 var msgOffset = psm.offsetTop;
442 smoothScroll(window.scrollY, msgOffset);
443 shake(psm, 10, 1000);
444 }
445 };
446
447 FormManager.prototype._fitField = function(evt) {
448 var ob = getTargetedObject(evt);
449 var inputWidth = ob.value.length / 1.9;
450 inputWidth = (inputWidth > 5) ? inputWidth : 5;
451 ob.style.width = inputWidth + 'em';
452 };
453
454 function initForms(baseElement, lazy) {
455 if (!baseElement) {
456 baseElement = document;
457 }
458 var dest = document.getElementById("mainCell");
459 var forms = baseElement.getElementsByTagName("form");
460 var f, i;
461 for (i = 0 ; i < forms.length ; i++ ) {
462 f = new FormManager(forms[i], dest, lazy);
463 }
464 }
465
466 function smoothScroll(from, to) {
467 var intervalId;
468 var step = 25;
469 var pos = from;
470 var dir;
471 if (to > from) {
472 dir = 1;
473 }
474 else {
475 dir = -1;
476 }
477
478 var jump = function() {
479 window.scroll(0, pos);
480 pos = pos + step * dir;
481 if ((dir === 1 && pos >= to) ||
482 (dir === -1 && pos <= to)) {
483 window.clearInterval(intervalId);
484 window.scroll(0, to);
485 }
486 };
487 intervalId = window.setInterval(jump, 10);
488 }
489
490 /* adapted from http://xahlee.info/js/js_shake_box.html */
491 shake = function(e, distance, time) {
492 // Handle arguments
493 if (!time) { time = 500; }
494 if (!distance) { distance = 5; }
495
496 // Save the original style of e, Make e relatively positioned, Note the animation start time, Start the animation
497 var originalStyle = e.style.cssText;
498 e.style.position = "relative";
499 var start = (new Date()).getTime();
500
501 // This function checks the elapsed time and updates the position of e.
502 // If the animation is complete, it restores e to its original state.
503 // Otherwise, it updates e's position and schedules itself to run again.
504 function animate() {
505 var now = (new Date()).getTime();
506 // Get current time
507 var elapsed = now-start;
508 // How long since we started
509 var fraction = elapsed/time;
510 // What fraction of total time?
511 if (fraction < 1) {
512 // If the animation is not yet complete
513 // Compute the x position of e as a function of animation
514 // completion fraction. We use a sinusoidal function, and multiply
515 // the completion fraction by 4pi, so that it shakes back and
516 // forth twice.
517 var x = distance * Math.sin(fraction*8*Math.PI);
518 e.style.left = x + "px";
519 // Try to run again in 25ms or at the end of the total time.
520 // We're aiming for a smooth 40 frames/second animation.
521 setTimeout(animate, Math.min(25, time-elapsed));
522 }
523 else {
524 // Otherwise, the animation is complete
525 e.style.cssText = originalStyle; // Restore the original style
526 }
527 }
528 animate();
529 };
530
531 }());