]> dev.renevier.net Git - syj.git/blob - public/js/utils.js
better mobile support
[syj.git] / public / js / utils.js
1 /*  This file is part of Syj, Copyright (c) 2010-2011 Arnaud Renevier,
2     and is published under the AGPL license. */
3
4 var CloseBtn = Class.create({
5     initialize: function(elt, options) {
6         var btn, imgsrc, style;
7
8         elt = $(elt);
9         if (!elt) {
10             return;
11         }
12
13         if (typeof options !== "object") {
14             options = {};
15         }
16
17         style = Object.extend({
18             'float': "right",
19             margin: "2px",
20             fontWeight: "bold",
21             padding: "0px"
22         }, options.style);
23
24         imgsrc = (options.closeBtnSrc) || "icons/close.png";
25         btn = new Element("input", { type: "image", src: imgsrc, alt: "X"}).setStyle(style);
26         elt.insert({top: btn});
27         btn.observe("click", function(evt) {
28             evt.stop();
29             if (evt.detail === 0) { // it's not a real click, possibly a submit event
30                 return;
31             }
32             if (typeof options.callback === "function") {
33                 options.callback.call(elt);
34             }
35             elt.hide();
36         });
37     }
38 });
39
40 var Toggler = Class.create({
41     options: {},
42
43     close: function() {
44         this.element.src = this.options.openIcn;
45         this.target.hide();
46         document.fire('toggler:close', this);
47     },
48
49     open: function() {
50         this.element.src = this.options.closeIcn;
51         this.target.show();
52         document.fire('toggler:open', this);
53     },
54
55     toggle: function(evt) {
56         if (evt && typeof evt.stop === "function") {
57             evt.stop();
58         }
59         if (this.target.visible()) {
60             this.close();
61         } else {
62             this.open();
63         }
64     },
65
66     initialize: function(target, options) {
67         this.options = Object.extend({
68                 openIcn: 'icons/bullet_arrow_right.png',
69                 closeIcn: 'icons/bullet_arrow_down.png'
70             }, options);
71
72         this.target = $(target).hide();
73         this.element = new Element("img").setStyle({ border: 'none',  // in firefox, in image inside an anchor has a border
74                                                     verticalAlign: "middle"});
75         this.element.observe('click', this.toggle.bindAsEventListener(this));
76
77         if (this.options.autoOpen) {
78             this.open();
79         } else {
80             this.close();
81         }
82     }
83 });
84
85 var Deck = Class.create({
86     initialize: function(elt, options) {
87         this.element = $(elt);
88         this.index = null;
89         this.setIndex(parseInt(this.element.readAttribute("selectedindex") || 0, 10));
90     },
91     setIndex: function(idx) {
92         if (idx === this.index) {
93             return;
94         }
95
96         var childs = this.element.childElements();
97         if (childs.length === 0) {
98             this.index = -1;
99             return;
100         }
101         idx = Math.max(0, idx);
102         idx = Math.min(childs.length - 1, idx);
103
104         childs.each(function(item, i) {
105             if (idx === i) {
106                 item.show();
107             } else {
108                 item.hide();
109             }
110         });
111         this.index = idx;
112     },
113     getIndex: function() {
114         return this.index;
115     }
116 });
117
118 Element.addMethods({
119     highlight: function(element, color, timeout) {
120         var current;
121         if (typeof timeout === "undefined") {
122             timeout = 0.3;
123         }
124         current = element.getStyle('backgroundColor');
125         Element.setStyle(element, {'backgroundColor': color});
126         Element.setStyle.delay(timeout, element, {'backgroundColor': current});
127         return element;
128     },
129     text: function(element, content) {
130         if (typeof content === "undefined") { // getter
131             if (element.nodeType === 8) {
132                 return "";
133             } else if (element.nodeType === 3 || element.nodeType === 4)  {
134                 return element.nodeValue;
135             } else {
136                 return $A(element.childNodes).inject("", function(acc, el) {
137                     return acc + Element.text(el);
138                  });
139             }
140         } else { // setter
141             var node = document.createTextNode(content);
142             element.update().appendChild(node);
143             return element;
144         }
145     }
146 });
147
148 Ajax.TimedRequest = Class.create(Ajax.Request, {
149     timeout: null,
150     delay: null,
151
152     abort: function() {
153         // see http://blog.pothoven.net/2007/12/aborting-ajax-requests-for-prototypejs.html
154         this.transport.onreadystatechange = Prototype.emptyFunction;
155         this.transport.abort();
156         Ajax.activeRequestCount--;
157     },
158
159     initialize: function($super, url, delay, options) {
160         this.delay = delay;
161         if (!options) {
162             options = {};
163         }
164
165         options.onSuccess = options.onSuccess &&
166             options.onSuccess.wrap(function(proceed, transport, json) {
167             if (this.timeout) {
168                 window.clearTimeout(this.timeout);
169                 this.timeout = null;
170             }
171             if (transport.getStatus() === 0) {
172                 this.options.onFailure(transport, json);
173             } else {
174                 proceed(transport, json);
175             }
176         }).bind(this);
177
178         options.onFailure = options.onFailure &&
179             options.onFailure.wrap(function(proceed, transport, json) {
180             if (this.timeout) {
181                 window.clearTimeout(this.timeout);
182                 this.timeout = null;
183             }
184             proceed(transport, json);
185         }).bind(this);
186
187         $super(url, options);
188     },
189
190     request: function($super, url) {
191         this.timeout = function() {
192             if (this.options.onFailure) {
193                 this.options.onFailure(null);
194             }
195             this.abort();
196         }.bind(this).delay(this.delay);
197         $super(url);
198     }
199 });
200
201 Ajax.Responders.register({
202     // needed for Ajax.TimedRequest.abort to work: see
203     // http://blog.pothoven.net/2007/12/aborting-ajax-requests-for-prototypejs.html
204     // again
205     onComplete: function() {
206         Ajax.activeRequestCount--;
207         if (Ajax.activeRequestCount < 0) {
208             Ajax.activeRequestCount = 0;
209         }
210     }
211 });
212
213 // wrapper around Form.request that sets up the submit listener, stops the
214 // submit event, calls presubmit function, calls Form.request and calls a
215 // postsubmit function. If form has some visible and activated file inputs,
216 // execute presubmit, but do not send the file with ajax.
217 Element.addMethods('form', {
218     ajaxize : function(form, options) {
219         var reqoptions;
220
221         options = Object.clone(options || {});
222
223         $(form).observe('submit', function(evt) {
224
225             reqoptions = Object.clone(options);
226             delete(reqoptions.presubmit);
227             delete(reqoptions.postsubmit);
228             delete(reqoptions.delay);
229
230             if (Object.isFunction(options.presubmit)) {
231                 if (options.presubmit(this) === false) {
232                     evt.stop(); // cancel form submission
233                     return;
234                 }
235             }
236
237             // get list of input file not disabled, and not hidden
238             if (this.getInputs('file').find(function(elt) {
239                 if (elt.disabled) {
240                     return false;
241                 }
242                 while (elt && $(elt).identify() !== this.identify()) {
243                     if (!elt.visible()) {
244                         return false;
245                     }
246                     elt = elt.parentNode;
247                 }
248                 return true;
249              }.bind(this))) {
250                 // form has some file inputs. Do not manage on our own.
251                 return;
252             }
253
254             evt.stop(); // cancel form submission
255
256             var params = reqoptions.parameters, action = this.readAttribute('action') || '';
257
258             if (action.blank()) {
259                 action = window.location.href;
260             }
261             reqoptions.parameters = this.serialize(true);
262
263             if (params) {
264                 if (Object.isString(params)) {
265                     params = params.toQueryParams();
266                 }
267                 Object.extend(reqoptions.parameters, params);
268             }
269
270             if (this.hasAttribute('method') && !reqoptions.method) {
271                 reqoptions.method = this.method;
272             }
273
274             if (reqoptions.onFailure) {
275                 reqoptions.onFailure = reqoptions.onFailure.wrap(function(proceed, transport, json) {
276                     form.enable();
277                     proceed(transport, json);
278                 });
279             } else {
280                 reqoptions.onFailure = function() {
281                     form.enable();
282                 };
283             }
284
285             if (reqoptions.onSuccess) {
286                 reqoptions.onSuccess = reqoptions.onSuccess.wrap(function(proceed, transport, json) {
287                     form.enable();
288                     proceed(transport, json);
289                 });
290             } else {
291                 reqoptions.onSuccess = function() {
292                     form.enable();
293                 };
294             }
295
296             new Ajax.TimedRequest(action, options.delay || 20, reqoptions);
297
298             if (Object.isFunction(options.postsubmit)) {
299                 options.postsubmit(this);
300             }
301             Form.getElements(form).each(function(elt) {
302                 elt.blur();
303                 elt.disable();
304             });
305         });
306     },
307
308     setfocus: function(form) {
309         var tofocus, error;
310
311         tofocus = null;
312         error = form.down('.error');
313         if (error) {
314             tofocus = error.previous('input,textarea');
315         } else {
316             tofocus = form.down('input:not([readonly],[disabled]),textarea:not([readonly][disabled])');
317         }
318         if (tofocus) {
319             if (error && (typeof tofocus.highlight === "function")) {
320                 tofocus.highlight('#F08080');
321             }
322             tofocus.activate();
323         }
324     },
325
326     checkEmptyElements: function(form, errorMessage) {
327         var results = [];
328         form.select('.required').each(function(elt) {
329             var id = elt.getAttribute('for'), control = $(id);
330             if (!control) {
331                 return;
332             }
333             if (!control.check(function() {
334                     return !this.value.strip().empty();
335                 }, errorMessage)) {
336                 results.push(control);
337             }
338         });
339         return results;
340     }
341 });
342
343 Element.addMethods(['input', 'textarea'], {
344     check: function(control, callback, errorMessage) {
345         if (callback.call(control)) {
346             return true;
347         }
348         control.insert({
349             after: new Element("div", {className: 'error'}).update(errorMessage)
350         });
351         return false;
352     },
353
354     observe : Element.Methods.observe.wrap(function(proceed, element, eventName, handler) {
355         if (eventName === "contentchange") {
356             proceed(element, 'keyup', function(evt) {
357                 if (evt.keyCode === 13) {
358                     return;
359                 }
360                 handler.apply(null, arguments);
361             });
362             proceed(element, 'paste', handler.defer.bind(handler));
363             return proceed(element, 'change', handler);
364         }
365         return proceed(element, eventName, handler);
366     }),
367
368     timedobserve: function(element, callback, delay) {
369         var timeout = null, initialvalue = element.value;
370
371         if (typeof delay !== "number") {
372             delay = 0.5;
373         }
374         delay = delay * 1000;
375
376         var canceltimer = function() {
377             if (timeout) {
378                 clearTimeout(timeout);
379                 timeout = null;
380             }
381         };
382         var resettimer = function() {
383             canceltimer();
384             timeout = setTimeout(triggercallback, delay);
385         };
386         var triggercallback = function() {
387             canceltimer();
388             if (initialvalue !== element.value) {
389                 initialvalue = element.value;
390                 callback.call(element);
391             }
392         };
393
394         element.observe('blur', triggercallback).
395              observe('keyup', resettimer).
396              observe('paste', resettimer);
397         return element;
398     }
399 });
400
401 Element.addMethods('div', {
402     setMessage: function(div, message, status) {
403         div.clearMessages();
404         if (status) {
405             div.setMessageStatus(status);
406         }
407         if (message) {
408             div.addMessage(message);
409         }
410         return div;
411     },
412
413     clearMessages: function(div) {
414         var node = div.firstChild, nextNode;
415
416         while (node) {
417             nextNode = node.nextSibling;
418             if (node.nodeType === 3 || node.tagName.toLowerCase() === 'br' || node.textContent || node.innerText) {
419                 div.removeChild(node);
420             }
421                 node = nextNode;
422         }
423
424         return div;
425     },
426
427     addMessage: function(div, message) {
428         var node = (div.ownerDocument || document).createTextNode(message);
429
430         if ($A(div.childNodes).filter(function(node) {
431                 return (node.nodeType === 3 || node.tagName.toLowerCase() === 'br' || node.textContent || node.innerText);
432              }).length) {
433             div.insert(new Element('br'));
434         }
435
436         div.appendChild(node);
437         return div.show();
438     },
439
440     setMessageStatus: function(div, status) {
441         $A(["error", "warn", "info", "success", "optional"]).each(function(clname) {
442             div.removeClassName(clname);
443         });
444         if (typeof status === "string") {
445             div.addClassName(status);
446         } else {
447             $A(status).each(function(clname) {
448                 div.addClassName(clname);
449             });
450         }
451         return div;
452     }
453 });