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