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