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