]> dev.renevier.net Git - syj.git/blob - public/js/utils.js
e2126f5950a9d553590c2e0731a59c9172f69a2e
[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             if (typeof elt.clearMessages === "function") {
36                 elt.clearMessages();
37             } else {
38                 elt.hide();
39             }
40         });
41     }
42 });
43
44 var Toggler = Class.create({
45     options: {},
46
47     close: function() {
48         this.element.src = this.options.openIcn;
49         this.target.hide();
50         document.fire('toggler:close', this);
51     },
52
53     open: function() {
54         this.element.src = this.options.closeIcn;
55         this.target.show();
56         document.fire('toggler:open', this);
57     },
58
59     toggle: function(evt) {
60         if (evt && typeof evt.stop === "function") {
61             evt.stop();
62         }
63         if (this.target.visible()) {
64             this.close();
65         } else {
66             this.open();
67         }
68     },
69
70     initialize: function(target, options) {
71         this.options = Object.extend({
72                 openIcn: 'icons/bullet_arrow_right.png',
73                 closeIcn: 'icons/bullet_arrow_down.png'
74             }, options);
75
76         this.target = $(target).hide();
77         this.element = new Element("img").setStyle({ border: 'none',  // in firefox, in image inside an anchor has a border
78                                                     verticalAlign: "middle"});
79         this.element.observe('click', this.toggle.bindAsEventListener(this));
80
81         if (this.options.autoOpen) {
82             this.open();
83         } else {
84             this.close();
85         }
86     }
87 });
88
89 var Deck = Class.create({
90     initialize: function(elt, options) {
91         this.element = $(elt);
92         this.index = null;
93         this.setIndex(parseInt(this.element.readAttribute("selectedindex") || 0, 10));
94     },
95     setIndex: function(idx) {
96         if (idx === this.index) {
97             return;
98         }
99
100         var childs = this.element.childElements();
101         if (childs.length === 0) {
102             this.index = -1;
103             return;
104         }
105         idx = Math.max(0, idx);
106         idx = Math.min(childs.length - 1, idx);
107
108         childs.each(function(item, i) {
109             if (idx === i) {
110                 item.show();
111             } else {
112                 item.hide();
113             }
114         });
115         this.index = idx;
116     },
117     getIndex: function() {
118         return this.index;
119     }
120 });
121
122 Element.addMethods({
123     highlight: function(element, color, timeout) {
124         var current;
125         if (typeof timeout === "undefined") {
126             timeout = 0.3;
127         }
128         current = element.getStyle('backgroundColor');
129         Element.setStyle(element, {'backgroundColor': color});
130         Element.setStyle.delay(timeout, element, {'backgroundColor': current});
131         return element;
132     },
133     text: function(element, content) {
134         if (typeof content === "undefined") { // getter
135             if (element.nodeType === 8) {
136                 return "";
137             } else if (element.nodeType === 3 || element.nodeType === 4)  {
138                 return element.nodeValue;
139             } else {
140                 return $A(element.childNodes).inject("", function(acc, el) {
141                     return acc + Element.text(el);
142                  });
143             }
144         } else { // setter
145             var node = document.createTextNode(content);
146             element.update().appendChild(node);
147             return element;
148         }
149     }
150 });
151
152 Ajax.TimedRequest = Class.create(Ajax.Request, {
153     timeout: null,
154     delay: null,
155
156     abort: function() {
157         // see http://blog.pothoven.net/2007/12/aborting-ajax-requests-for-prototypejs.html
158         this.transport.onreadystatechange = Prototype.emptyFunction;
159         this.transport.abort();
160         Ajax.activeRequestCount--;
161     },
162
163     initialize: function($super, url, delay, options) {
164         this.delay = delay;
165         if (!options) {
166             options = {};
167         }
168
169         options.onSuccess = options.onSuccess &&
170             options.onSuccess.wrap(function(proceed, transport, json) {
171             if (this.timeout) {
172                 window.clearTimeout(this.timeout);
173                 this.timeout = null;
174             }
175             if (transport.getStatus() === 0) {
176                 this.options.onFailure(transport, json);
177             } else {
178                 proceed(transport, json);
179             }
180         }).bind(this);
181
182         options.onFailure = options.onFailure &&
183             options.onFailure.wrap(function(proceed, transport, json) {
184             if (this.timeout) {
185                 window.clearTimeout(this.timeout);
186                 this.timeout = null;
187             }
188             proceed(transport, json);
189         }).bind(this);
190
191         $super(url, options);
192     },
193
194     request: function($super, url) {
195         this.timeout = function() {
196             if (this.options.onFailure) {
197                 this.options.onFailure(null);
198             }
199             this.abort();
200         }.bind(this).delay(this.delay);
201         $super(url);
202     }
203 });
204
205 Ajax.Responders.register({
206     // needed for Ajax.TimedRequest.abort to work: see
207     // http://blog.pothoven.net/2007/12/aborting-ajax-requests-for-prototypejs.html
208     // again
209     onComplete: function() {
210         Ajax.activeRequestCount--;
211         if (Ajax.activeRequestCount < 0) {
212             Ajax.activeRequestCount = 0;
213         }
214     }
215 });
216
217 // wrapper around Form.request that sets up the submit listener, stops the
218 // submit event, calls presubmit function, calls Form.request and calls a
219 // postsubmit function. If form has some visible and activated file inputs,
220 // execute presubmit, but do not send the file with ajax.
221 Element.addMethods('form', {
222     ajaxize : function(form, options) {
223         var reqoptions;
224
225         options = Object.clone(options || {});
226
227         $(form).observe('submit', function(evt) {
228
229             reqoptions = Object.clone(options);
230             delete(reqoptions.presubmit);
231             delete(reqoptions.postsubmit);
232             delete(reqoptions.delay);
233
234             if (Object.isFunction(options.presubmit)) {
235                 if (options.presubmit(this) === false) {
236                     evt.stop(); // cancel form submission
237                     return;
238                 }
239             }
240
241             // get list of input file not disabled, and not hidden
242             if (this.getInputs('file').find(function(elt) {
243                 if (elt.disabled) {
244                     return false;
245                 }
246                 while (elt && $(elt).identify() !== this.identify()) {
247                     if (!elt.visible()) {
248                         return false;
249                     }
250                     elt = elt.parentNode;
251                 }
252                 return true;
253              }.bind(this))) {
254                 // form has some file inputs. Do not manage on our own.
255                 return;
256             }
257
258             evt.stop(); // cancel form submission
259
260             var params = reqoptions.parameters, action = this.readAttribute('action') || '';
261
262             if (action.blank()) {
263                 action = window.location.href;
264             }
265             reqoptions.parameters = this.serialize(true);
266
267             if (params) {
268                 if (Object.isString(params)) {
269                     params = params.toQueryParams();
270                 }
271                 Object.extend(reqoptions.parameters, params);
272             }
273
274             if (this.hasAttribute('method') && !reqoptions.method) {
275                 reqoptions.method = this.method;
276             }
277
278             if (reqoptions.onFailure) {
279                 reqoptions.onFailure = reqoptions.onFailure.wrap(function(proceed, transport, json) {
280                     form.enable();
281                     proceed(transport, json);
282                 });
283             } else {
284                 reqoptions.onFailure = function() {
285                     form.enable();
286                 };
287             }
288
289             if (reqoptions.onSuccess) {
290                 reqoptions.onSuccess = reqoptions.onSuccess.wrap(function(proceed, transport, json) {
291                     form.enable();
292                     proceed(transport, json);
293                 });
294             } else {
295                 reqoptions.onSuccess = function() {
296                     form.enable();
297                 };
298             }
299
300             new Ajax.TimedRequest(action, options.delay || 20, reqoptions);
301
302             if (Object.isFunction(options.postsubmit)) {
303                 options.postsubmit(this);
304             }
305             Form.getElements(form).each(function(elt) {
306                 elt.blur();
307                 elt.disable();
308             });
309         });
310     },
311
312     setfocus: function(form) {
313         var tofocus, error;
314
315         tofocus = null;
316         error = form.down('.error');
317         if (error) {
318             tofocus = error.previous('input,textarea');
319         } else {
320             tofocus = form.down('input:not([readonly],[disabled]),textarea:not([readonly][disabled])');
321         }
322         if (tofocus) {
323             if (error && (typeof tofocus.highlight === "function")) {
324                 tofocus.highlight('#F08080');
325             }
326             tofocus.activate();
327         }
328     },
329
330     checkEmptyElements: function(form, errorMessage) {
331         var results = [];
332         form.select('.required').each(function(elt) {
333             var id = elt.getAttribute('for'), control = $(id);
334             if (!control) {
335                 return;
336             }
337             if (!control.check(function() {
338                     return !this.value.strip().empty();
339                 }, errorMessage)) {
340                 results.push(control);
341             }
342         });
343         return results;
344     }
345 });
346
347 Element.addMethods(['input', 'textarea'], {
348     check: function(control, callback, errorMessage) {
349         if (callback.call(control)) {
350             return true;
351         }
352         control.insert({
353             after: new Element("div", {className: 'error'}).update(errorMessage)
354         });
355         return false;
356     },
357
358     observe : Element.Methods.observe.wrap(function(proceed, element, eventName, handler) {
359         if (eventName === "contentchange") {
360             proceed(element, 'keyup', function(evt) {
361                 if (evt.keyCode === 13) {
362                     return;
363                 }
364                 handler.apply(null, arguments);
365             });
366             proceed(element, 'paste', handler.defer.bind(handler));
367             return proceed(element, 'change', handler);
368         }
369         return proceed(element, eventName, handler);
370     }),
371
372     timedobserve: function(element, callback, delay) {
373         var timeout = null, initialvalue = element.value;
374
375         if (typeof delay !== "number") {
376             delay = 0.5;
377         }
378         delay = delay * 1000;
379
380         var canceltimer = function() {
381             if (timeout) {
382                 clearTimeout(timeout);
383                 timeout = null;
384             }
385         };
386         var resettimer = function() {
387             canceltimer();
388             timeout = setTimeout(triggercallback, delay);
389         };
390         var triggercallback = function() {
391             canceltimer();
392             if (initialvalue !== element.value) {
393                 initialvalue = element.value;
394                 callback.call(element);
395             }
396         };
397
398         element.observe('blur', triggercallback).
399              observe('keyup', resettimer).
400              observe('paste', resettimer);
401         return element;
402     }
403 });
404
405 Element.addMethods('div', (function() {
406     var supportsTransition = false, endTransitionEventName = null;
407
408     if (window.addEventListener) { // fails badly in ie: prevents page from loading
409         var div = $(document.createElement('div'));
410         var timeout = null;
411
412         var cleanup = function() {
413             if (timeout) {
414                 window.clearTimeout(timeout);
415                 timeout = null;
416                 div.stopObserving('webkitTransitionEnd');
417                 div.stopObserving('transitionend');
418                 div.stopObserving('oTransitionend');
419                 Element.remove.defer(div);
420             }
421         }
422
423         var handler = function(e) {
424             supportsTransition = true;
425             endTransitionEventName = e.type;
426             cleanup();
427         }
428         div.observe('webkitTransitionEnd', handler).observe('transitionend', handler) .observe('oTransitionend', handler);
429         div.setStyle({'transitionProperty': 'opacity',
430                       'MozTransitionProperty': 'opacity',
431                       'WebkitTransitionProperty': 'opacity',
432                       'OTransitionProperty': 'opacity',
433                       'transitionDuration': '1ms',
434                       'MozTransitionDuration': '1ms',
435                       'WebkitTransitionDuration': '1ms',
436                       'OTransitionDuration': '1ms'});
437         $(document.documentElement).insert(div);
438         Element.setOpacity.defer(div, 0);
439         window.setTimeout(cleanup, 100);
440     }
441
442     function removeMessages(div) {
443         var node = div.firstChild, nextNode;
444
445         while (node) {
446             nextNode = node.nextSibling;
447             if (node.nodeType === 3 || node.tagName.toLowerCase() === 'br' || node.textContent || node.innerText) {
448                 div.removeChild(node);
449             }
450                 node = nextNode;
451         }
452         return div;
453     };
454
455     function hasOpacityTransition(div) {
456         return ([div.getStyle('transition-property'),
457                  div.getStyle('-moz-transition-property'),
458                  div.getStyle('-webkit-transition-property'),
459                  div.getStyle('-o-transition-property')
460                  ].join(' ').split(' ').indexOf('opacity') !== -1);
461     }
462
463     function hide(div) {
464         div = $(div);
465         if (supportsTransition && hasOpacityTransition(div)) {
466             div.observe(endTransitionEventName, function() {
467                 div.stopObserving(endTransitionEventName);
468                 div.hide();
469             });
470             div.setOpacity(0);
471         } else {
472             div.hide();
473         }
474     }
475
476     function show(div) {
477         div = $(div);
478         div.show();
479         // we need to set opacity to 0 before calling hasOpacityTransition
480         // otherwise we trigger mozilla #601190
481         div.setOpacity(0);
482         if (supportsTransition && hasOpacityTransition(div)) {
483             // display = '' then opacity = 1;
484             Element.setOpacity.defer(div, 1);
485         } else {
486             div.setOpacity(1);
487         }
488     }
489
490     function clearMessages(div) {
491         if (div.visible()) {
492             hide(div);
493         }
494         return div;
495     }
496
497     function setMessage(div, message, status) {
498         removeMessages(div);
499         if (status) {
500             div.setMessageStatus(status);
501         }
502         if (message) {
503             div.addMessage(message);
504         }
505         return div;
506     }
507
508     function addMessage(div, message) {
509         var node = (div.ownerDocument || document).createTextNode(message);
510
511         if ($A(div.childNodes).filter(function(node) {
512                 return (node.nodeType === 3 || node.tagName.toLowerCase() === 'br' || node.textContent || node.innerText);
513              }).length) {
514             div.insert(new Element('br'));
515         }
516
517         div.appendChild(node);
518         if (!div.visible()) {
519             show(div);
520         }
521         return div;
522     }
523
524     function setMessageStatus(div, status) {
525         $A(["error", "warn", "info", "success", "optional"]).each(function(clname) {
526             div.removeClassName(clname);
527         });
528         if (typeof status === "string") {
529             div.addClassName(status);
530         } else {
531             $A(status).each(function(clname) {
532                 div.addClassName(clname);
533             });
534         }
535         return div;
536     }
537
538     return {
539         setMessage: setMessage,
540         clearMessages: clearMessages,
541         addMessage: addMessage,
542         setMessageStatus: setMessageStatus
543     };
544
545 })());