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