Added Gruntfile with jshint and corrected most js errors
[odoo/odoo.git] / addons / web / static / src / js / corelib.js
1 /*
2  * Copyright (c) 2012, OpenERP S.A.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice, this
9  *    list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright notice,
11  *    this list of conditions and the following disclaimer in the documentation
12  *    and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 openerp.web.corelib = function(instance) {
27
28 /**
29  * Improved John Resig's inheritance, based on:
30  *
31  * Simple JavaScript Inheritance
32  * By John Resig http://ejohn.org/
33  * MIT Licensed.
34  *
35  * Adds "include()"
36  *
37  * Defines The Class object. That object can be used to define and inherit classes using
38  * the extend() method.
39  *
40  * Example:
41  *
42  * var Person = instance.web.Class.extend({
43  *  init: function(isDancing){
44  *     this.dancing = isDancing;
45  *   },
46  *   dance: function(){
47  *     return this.dancing;
48  *   }
49  * });
50  *
51  * The init() method act as a constructor. This class can be instancied this way:
52  *
53  * var person = new Person(true);
54  * person.dance();
55  *
56  * The Person class can also be extended again:
57  *
58  * var Ninja = Person.extend({
59  *   init: function(){
60  *     this._super( false );
61  *   },
62  *   dance: function(){
63  *     // Call the inherited version of dance()
64  *     return this._super();
65  *   },
66  *   swingSword: function(){
67  *     return true;
68  *   }
69  * });
70  *
71  * When extending a class, each re-defined method can use this._super() to call the previous
72  * implementation of that method.
73  */
74 (function() {
75     var initializing = false,
76         fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
77     // The web Class implementation (does nothing)
78     instance.web.Class = function(){};
79
80     /**
81      * Subclass an existing class
82      *
83      * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
84      */
85     instance.web.Class.extend = function() {
86         var _super = this.prototype;
87         // Support mixins arguments
88         var args = _.toArray(arguments);
89         args.unshift({});
90         var prop = _.extend.apply(_,args);
91
92         // Instantiate a web class (but only create the instance,
93         // don't run the init constructor)
94         initializing = true;
95         var prototype = new this();
96         initializing = false;
97
98         // Copy the properties over onto the new prototype
99         for (var name in prop) {
100             // Check if we're overwriting an existing function
101             prototype[name] = typeof prop[name] == "function" &&
102                               fnTest.test(prop[name]) ?
103                     (function(name, fn) {
104                         return function() {
105                             var tmp = this._super;
106
107                             // Add a new ._super() method that is the same
108                             // method but on the super-class
109                             this._super = _super[name];
110
111                             // The method only need to be bound temporarily, so
112                             // we remove it when we're done executing
113                             var ret = fn.apply(this, arguments);
114                             this._super = tmp;
115
116                             return ret;
117                         };
118                     })(name, prop[name]) :
119                     prop[name];
120         }
121
122         // The dummy class constructor
123         function Class() {
124             if(this.constructor !== instance.web.Class){
125                 throw new Error("You can only instanciate objects with the 'new' operator");
126             }
127             // All construction is actually done in the init method
128             if (!initializing && this.init) {
129                 var ret = this.init.apply(this, arguments);
130                 if (ret) { return ret; }
131             }
132             return this;
133         }
134         Class.include = function (properties) {
135             for (var name in properties) {
136                 if (typeof properties[name] !== 'function'
137                         || !fnTest.test(properties[name])) {
138                     prototype[name] = properties[name];
139                 } else if (typeof prototype[name] === 'function'
140                            && prototype.hasOwnProperty(name)) {
141                     prototype[name] = (function (name, fn, previous) {
142                         return function () {
143                             var tmp = this._super;
144                             this._super = previous;
145                             var ret = fn.apply(this, arguments);
146                             this._super = tmp;
147                             return ret;
148                         }
149                     })(name, properties[name], prototype[name]);
150                 } else if (typeof _super[name] === 'function') {
151                     prototype[name] = (function (name, fn) {
152                         return function () {
153                             var tmp = this._super;
154                             this._super = _super[name];
155                             var ret = fn.apply(this, arguments);
156                             this._super = tmp;
157                             return ret;
158                         }
159                     })(name, properties[name]);
160                 }
161             }
162         };
163
164         // Populate our constructed prototype object
165         Class.prototype = prototype;
166
167         // Enforce the constructor to be what we expect
168         Class.constructor = Class;
169
170         // And make this class extendable
171         Class.extend = arguments.callee;
172
173         return Class;
174     };
175 })();
176
177 // Mixins
178
179 /**
180  * Mixin to structure objects' life-cycles folowing a parent-children
181  * relationship. Each object can a have a parent and multiple children.
182  * When an object is destroyed, all its children are destroyed too releasing
183  * any resource they could have reserved before.
184  */
185 instance.web.ParentedMixin = {
186     __parentedMixin : true,
187     init: function() {
188         this.__parentedDestroyed = false;
189         this.__parentedChildren = [];
190         this.__parentedParent = null;
191     },
192     /**
193      * Set the parent of the current object. When calling this method, the
194      * parent will also be informed and will return the current object
195      * when its getChildren() method is called. If the current object did
196      * already have a parent, it is unregistered before, which means the
197      * previous parent will not return the current object anymore when its
198      * getChildren() method is called.
199      */
200     setParent : function(parent) {
201         if (this.getParent()) {
202             if (this.getParent().__parentedMixin) {
203                 this.getParent().__parentedChildren = _.without(this
204                         .getParent().getChildren(), this);
205             }
206         }
207         this.__parentedParent = parent;
208         if (parent && parent.__parentedMixin) {
209             parent.__parentedChildren.push(this);
210         }
211     },
212     /**
213      * Return the current parent of the object (or null).
214      */
215     getParent : function() {
216         return this.__parentedParent;
217     },
218     /**
219      * Return a list of the children of the current object.
220      */
221     getChildren : function() {
222         return _.clone(this.__parentedChildren);
223     },
224     /**
225      * Returns true if destroy() was called on the current object.
226      */
227     isDestroyed : function() {
228         return this.__parentedDestroyed;
229     },
230     /**
231         Utility method to only execute asynchronous actions if the current
232         object has not been destroyed.
233
234         @param {$.Deferred} promise The promise representing the asynchronous
235                                     action.
236         @param {bool} [reject=false] If true, the returned promise will be
237                                      rejected with no arguments if the current
238                                      object is destroyed. If false, the
239                                      returned promise will never be resolved
240                                      or rejected.
241         @returns {$.Deferred} A promise that will mirror the given promise if
242                               everything goes fine but will either be rejected
243                               with no arguments or never resolved if the
244                               current object is destroyed.
245     */
246     alive: function(promise, reject) {
247         var def = $.Deferred();
248         var self = this;
249         promise.done(function() {
250             if (! self.isDestroyed()) {
251                 if (! reject)
252                     def.resolve.apply(def, arguments);
253                 else
254                     def.reject();
255             }
256         }).fail(function() {
257             if (! self.isDestroyed()) {
258                 if (! reject)
259                     def.reject.apply(def, arguments);
260                 else
261                     def.reject();
262             }
263         });
264         return def.promise();
265     },
266     /**
267      * Inform the object it should destroy itself, releasing any
268      * resource it could have reserved.
269      */
270     destroy : function() {
271         _.each(this.getChildren(), function(el) {
272             el.destroy();
273         });
274         this.setParent(undefined);
275         this.__parentedDestroyed = true;
276     }
277 };
278
279 /**
280  * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
281  *
282  * This class just handle the dispatching of events, it is not meant to be extended,
283  * nor used directly. All integration with parenting and automatic unregistration of
284  * events is done in EventDispatcherMixin.
285  *
286  * Copyright notice for the following Class:
287  *
288  * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
289  * Backbone may be freely distributed under the MIT license.
290  * For all details and documentation:
291  * http://backbonejs.org
292  *
293  */
294 var Events = instance.web.Class.extend({
295     on : function(events, callback, context) {
296         var ev;
297         events = events.split(/\s+/);
298         var calls = this._callbacks || (this._callbacks = {});
299         while ((ev = events.shift())) {
300             var list = calls[ev] || (calls[ev] = {});
301             var tail = list.tail || (list.tail = list.next = {});
302             tail.callback = callback;
303             tail.context = context;
304             list.tail = tail.next = {};
305         }
306         return this;
307     },
308
309     off : function(events, callback, context) {
310         var ev, calls, node;
311         if (!events) {
312             delete this._callbacks;
313         } else if ((calls = this._callbacks)) {
314             events = events.split(/\s+/);
315             while ((ev = events.shift())) {
316                 node = calls[ev];
317                 delete calls[ev];
318                 if (!callback || !node)
319                     continue;
320                 while ((node = node.next) && node.next) {
321                     if (node.callback === callback
322                             && (!context || node.context === context))
323                         continue;
324                     this.on(ev, node.callback, node.context);
325                 }
326             }
327         }
328         return this;
329     },
330
331     callbackList: function() {
332         var lst = [];
333         _.each(this._callbacks || {}, function(el, eventName) {
334             var node = el;
335             while ((node = node.next) && node.next) {
336                 lst.push([eventName, node.callback, node.context]);
337             }
338         });
339         return lst;
340     },
341
342     trigger : function(events) {
343         var event, node, calls, tail, args, all, rest;
344         if (!(calls = this._callbacks))
345             return this;
346         all = calls['all'];
347         (events = events.split(/\s+/)).push(null);
348         // Save references to the current heads & tails.
349         while ((event = events.shift())) {
350             if (all)
351                 events.push({
352                     next : all.next,
353                     tail : all.tail,
354                     event : event
355                 });
356             if (!(node = calls[event]))
357                 continue;
358             events.push({
359                 next : node.next,
360                 tail : node.tail
361             });
362         }
363         rest = Array.prototype.slice.call(arguments, 1);
364         while ((node = events.pop())) {
365             tail = node.tail;
366             args = node.event ? [ node.event ].concat(rest) : rest;
367             while ((node = node.next) !== tail) {
368                 node.callback.apply(node.context || this, args);
369             }
370         }
371         return this;
372     }
373 });
374
375 instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, {
376     __eventDispatcherMixin: true,
377     init: function() {
378         instance.web.ParentedMixin.init.call(this);
379         this.__edispatcherEvents = new Events();
380         this.__edispatcherRegisteredEvents = [];
381     },
382     on: function(events, dest, func) {
383         var self = this;
384         if (!(func instanceof Function)) {
385             throw new Error("Event handler must be a function.");
386         }
387         events = events.split(/\s+/);
388         _.each(events, function(eventName) {
389             self.__edispatcherEvents.on(eventName, func, dest);
390             if (dest && dest.__eventDispatcherMixin) {
391                 dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
392             }
393         });
394         return this;
395     },
396     off: function(events, dest, func) {
397         var self = this;
398         events = events.split(/\s+/);
399         _.each(events, function(eventName) {
400             self.__edispatcherEvents.off(eventName, func, dest);
401             if (dest && dest.__eventDispatcherMixin) {
402                 dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
403                     return !(el.name === eventName && el.func === func && el.source === self);
404                 });
405             }
406         });
407         return this;
408     },
409     trigger: function(events) {
410         this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
411         return this;
412     },
413     destroy: function() {
414         var self = this;
415         _.each(this.__edispatcherRegisteredEvents, function(event) {
416             event.source.__edispatcherEvents.off(event.name, event.func, self);
417         });
418         this.__edispatcherRegisteredEvents = [];
419         _.each(this.__edispatcherEvents.callbackList(), function(cal) {
420             this.off(cal[0], cal[2], cal[1]);
421         }, this);
422         this.__edispatcherEvents.off();
423         instance.web.ParentedMixin.destroy.call(this);
424     }
425 });
426
427 instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, {
428     init: function() {
429         instance.web.EventDispatcherMixin.init.call(this);
430         this.__getterSetterInternalMap = {};
431     },
432     set: function(arg1, arg2, arg3) {
433         var map;
434         var options;
435         if (typeof arg1 === "string") {
436             map = {};
437             map[arg1] = arg2;
438             options = arg3 || {};
439         } else {
440             map = arg1;
441             options = arg2 || {};
442         }
443         var self = this;
444         var changed = false;
445         _.each(map, function(val, key) {
446             var tmp = self.__getterSetterInternalMap[key];
447             if (tmp === val)
448                 return;
449             changed = true;
450             self.__getterSetterInternalMap[key] = val;
451             if (! options.silent)
452                 self.trigger("change:" + key, self, {
453                     oldValue: tmp,
454                     newValue: val
455                 });
456         });
457         if (changed)
458             self.trigger("change", self);
459     },
460     get: function(key) {
461         return this.__getterSetterInternalMap[key];
462     }
463 });
464
465 // Classes
466
467 /**
468     A class containing common utility methods useful when working with OpenERP as well as the PropertiesMixin.
469 */
470 instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin, {
471     /**
472      * Constructs the object and sets its parent if a parent is given.
473      *
474      * @param {instance.web.Controller} parent Binds the current instance to the given Controller instance.
475      * When that controller is destroyed by calling destroy(), the current instance will be
476      * destroyed too. Can be null.
477      */
478     init: function(parent) {
479         instance.web.PropertiesMixin.init.call(this);
480         this.setParent(parent);
481     },
482     /**
483      * Proxies a method of the object, in order to keep the right ``this`` on
484      * method invocations.
485      *
486      * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
487      * even more so to ``jQuery.proxy`` with a fundamental difference: its
488      * resolution of the method being called is lazy, meaning it will use the
489      * method as it is when the proxy is called, not when the proxy is created.
490      *
491      * Other methods will fix the bound method to what it is when creating the
492      * binding/proxy, which is fine in most javascript code but problematic in
493      * OpenERP Web where developers may want to replace existing callbacks with
494      * theirs.
495      *
496      * The semantics of this precisely replace closing over the method call.
497      *
498      * @param {String|Function} method function or name of the method to invoke
499      * @returns {Function} proxied method
500      */
501     proxy: function (method) {
502         var self = this;
503         return function () {
504             var fn = (typeof method === 'string') ? self[method] : method;
505             return fn.apply(self, arguments);
506         }
507     },
508     /**
509      * Informs the action manager to do an action. This supposes that
510      * the action manager can be found amongst the ancestors of the current widget.
511      * If that's not the case this method will simply return `false`.
512      */
513     do_action: function() {
514         var parent = this.getParent();
515         if (parent) {
516             return parent.do_action.apply(parent, arguments);
517         }
518         return false;
519     },
520     do_notify: function() {
521         if (this.getParent()) {
522             return this.getParent().do_notify.apply(this,arguments);
523         }
524         return false;
525     },
526     do_warn: function() {
527         if (this.getParent()) {
528             return this.getParent().do_warn.apply(this,arguments);
529         }
530         return false;
531     },
532     rpc: function(url, data, options) {
533         return this.alive(instance.session.rpc(url, data, options));
534     }
535 });
536
537 /**
538  * Base class for all visual components. Provides a lot of functionalities helpful
539  * for the management of a part of the DOM.
540  *
541  * Widget handles:
542  * - Rendering with QWeb.
543  * - Life-cycle management and parenting (when a parent is destroyed, all its children are
544  *     destroyed too).
545  * - Insertion in DOM.
546  *
547  * Guide to create implementations of the Widget class:
548  * ==============================================
549  *
550  * Here is a sample child class:
551  *
552  * MyWidget = instance.base.Widget.extend({
553  *     // the name of the QWeb template to use for rendering
554  *     template: "MyQWebTemplate",
555  *
556  *     init: function(parent) {
557  *         this._super(parent);
558  *         // stuff that you want to init before the rendering
559  *     },
560  *     start: function() {
561  *         // stuff you want to make after the rendering, `this.$el` holds a correct value
562  *         this.$el.find(".my_button").click(/* an example of event binding * /);
563  *
564  *         // if you have some asynchronous operations, it's a good idea to return
565  *         // a promise in start()
566  *         var promise = this.rpc(...);
567  *         return promise;
568  *     }
569  * });
570  *
571  * Now this class can simply be used with the following syntax:
572  *
573  * var my_widget = new MyWidget(this);
574  * my_widget.appendTo($(".some-div"));
575  *
576  * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
577  * DOM inside the ".some-div" div and its events were binded.
578  *
579  * And of course, when you don't need that widget anymore, just do:
580  *
581  * my_widget.destroy();
582  *
583  * That will kill the widget in a clean way and erase its content from the dom.
584  */
585 instance.web.Widget = instance.web.Controller.extend({
586     // Backbone-ish API
587     tagName: 'div',
588     id: null,
589     className: null,
590     attributes: {},
591     events: {},
592     /**
593      * The name of the QWeb template that will be used for rendering. Must be
594      * redefined in subclasses or the default render() method can not be used.
595      *
596      * @type string
597      */
598     template: null,
599     /**
600      * Constructs the widget and sets its parent if a parent is given.
601      *
602      * @constructs instance.web.Widget
603      *
604      * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
605      * When that widget is destroyed by calling destroy(), the current instance will be
606      * destroyed too. Can be null.
607      */
608     init: function(parent) {
609         this._super(parent);
610         // Bind on_/do_* methods to this
611         // We might remove this automatic binding in the future
612         for (var name in this) {
613             if(typeof(this[name]) == "function") {
614                 if((/^on_|^do_/).test(name)) {
615                     this[name] = this[name].bind(this);
616                 }
617             }
618         }
619         // FIXME: this should not be
620         this.setElement(this._make_descriptive());
621         this.session = instance.session;
622     },
623     /**
624      * Destroys the current widget, also destroys all its children before destroying itself.
625      */
626     destroy: function() {
627         _.each(this.getChildren(), function(el) {
628             el.destroy();
629         });
630         if(this.$el) {
631             this.$el.remove();
632         }
633         instance.web.PropertiesMixin.destroy.call(this);
634     },
635     /**
636      * Renders the current widget and appends it to the given jQuery object or Widget.
637      *
638      * @param target A jQuery object or a Widget instance.
639      */
640     appendTo: function(target) {
641         var self = this;
642         return this.__widgetRenderAndInsert(function(t) {
643             self.$el.appendTo(t);
644         }, target);
645     },
646     /**
647      * Renders the current widget and prepends it to the given jQuery object or Widget.
648      *
649      * @param target A jQuery object or a Widget instance.
650      */
651     prependTo: function(target) {
652         var self = this;
653         return this.__widgetRenderAndInsert(function(t) {
654             self.$el.prependTo(t);
655         }, target);
656     },
657     /**
658      * Renders the current widget and inserts it after to the given jQuery object or Widget.
659      *
660      * @param target A jQuery object or a Widget instance.
661      */
662     insertAfter: function(target) {
663         var self = this;
664         return this.__widgetRenderAndInsert(function(t) {
665             self.$el.insertAfter(t);
666         }, target);
667     },
668     /**
669      * Renders the current widget and inserts it before to the given jQuery object or Widget.
670      *
671      * @param target A jQuery object or a Widget instance.
672      */
673     insertBefore: function(target) {
674         var self = this;
675         return this.__widgetRenderAndInsert(function(t) {
676             self.$el.insertBefore(t);
677         }, target);
678     },
679     /**
680      * Renders the current widget and replaces the given jQuery object.
681      *
682      * @param target A jQuery object or a Widget instance.
683      */
684     replace: function(target) {
685         return this.__widgetRenderAndInsert(_.bind(function(t) {
686             this.$el.replaceAll(t);
687         }, this), target);
688     },
689     __widgetRenderAndInsert: function(insertion, target) {
690         this.renderElement();
691         insertion(target);
692         return this.start();
693     },
694     /**
695      * Method called after rendering. Mostly used to bind actions, perform asynchronous
696      * calls, etc...
697      *
698      * By convention, this method should return an object that can be passed to $.when() 
699      * to inform the caller when this widget has been initialized.
700      *
701      * @returns {jQuery.Deferred or any}
702      */
703     start: function() {
704         return $.when();
705     },
706     /**
707      * Renders the element. The default implementation renders the widget using QWeb,
708      * `this.template` must be defined. The context given to QWeb contains the "widget"
709      * key that references `this`.
710      */
711     renderElement: function() {
712         var $el;
713         if (this.template) {
714             $el = $(_.str.trim(instance.web.qweb.render(
715                 this.template, {widget: this})));
716         } else {
717             $el = this._make_descriptive();
718         }
719         this.replaceElement($el);
720     },
721     /**
722      * Re-sets the widget's root element and replaces the old root element
723      * (if any) by the new one in the DOM.
724      *
725      * @param {HTMLElement | jQuery} $el
726      * @returns {*} this
727      */
728     replaceElement: function ($el) {
729         var $oldel = this.$el;
730         this.setElement($el);
731         if ($oldel && !$oldel.is(this.$el)) {
732             $oldel.replaceWith(this.$el);
733         }
734         return this;
735     },
736     /**
737      * Re-sets the widget's root element (el/$el/$el).
738      *
739      * Includes:
740      * * re-delegating events
741      * * re-binding sub-elements
742      * * if the widget already had a root element, replacing the pre-existing
743      *   element in the DOM
744      *
745      * @param {HTMLElement | jQuery} element new root element for the widget
746      * @return {*} this
747      */
748     setElement: function (element) {
749         // NB: completely useless, as WidgetMixin#init creates a $el
750         // always
751         if (this.$el) {
752             this.undelegateEvents();
753         }
754
755         this.$el = (element instanceof $) ? element : $(element);
756         this.el = this.$el[0];
757
758         this.delegateEvents();
759
760         return this;
761     },
762     /**
763      * Utility function to build small DOM elements.
764      *
765      * @param {String} tagName name of the DOM element to create
766      * @param {Object} [attributes] map of DOM attributes to set on the element
767      * @param {String} [content] HTML content to set on the element
768      * @return {Element}
769      */
770     make: function (tagName, attributes, content) {
771         var el = document.createElement(tagName);
772         if (!_.isEmpty(attributes)) {
773             $(el).attr(attributes);
774         }
775         if (content) {
776             $(el).html(content);
777         }
778         return el;
779     },
780     /**
781      * Makes a potential root element from the declarative builder of the
782      * widget
783      *
784      * @return {jQuery}
785      * @private
786      */
787     _make_descriptive: function () {
788         var attrs = _.extend({}, this.attributes || {});
789         if (this.id) { attrs.id = this.id; }
790         if (this.className) { attrs['class'] = this.className; }
791         return $(this.make(this.tagName, attrs));
792     },
793     delegateEvents: function () {
794         var events = this.events;
795         if (_.isEmpty(events)) { return; }
796
797         for(var key in events) {
798             if (!events.hasOwnProperty(key)) { continue; }
799
800             var method = this.proxy(events[key]);
801
802             var match = /^(\S+)(\s+(.*))?$/.exec(key);
803             var event = match[1];
804             var selector = match[3];
805
806             event += '.widget_events';
807             if (!selector) {
808                 this.$el.on(event, method);
809             } else {
810                 this.$el.on(event, selector, method);
811             }
812         }
813     },
814     undelegateEvents: function () {
815         this.$el.off('.widget_events');
816     },
817     /**
818      * Shortcut for ``this.$el.find(selector)``
819      *
820      * @param {String} selector CSS selector, rooted in $el
821      * @returns {jQuery} selector match
822      */
823     $: function(selector) {
824         return this.$el.find(selector);
825     }
826 });
827
828 instance.web.Registry = instance.web.Class.extend({
829     /**
830      * Stores a mapping of arbitrary key (strings) to object paths (as strings
831      * as well).
832      *
833      * Resolves those paths at query time in order to always fetch the correct
834      * object, even if those objects have been overloaded/replaced after the
835      * registry was created.
836      *
837      * An object path is simply a dotted name from the instance root to the
838      * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP
839      * session object).
840      *
841      * @constructs instance.web.Registry
842      * @param {Object} mapping a mapping of keys to object-paths
843      */
844     init: function (mapping) {
845         this.parent = null;
846         this.map = mapping || {};
847     },
848     /**
849      * Retrieves the object matching the provided key string.
850      *
851      * @param {String} key the key to fetch the object for
852      * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
853      * @returns {Class} the stored class, to initialize or null if not found
854      */
855     get_object: function (key, silent_error) {
856         var path_string = this.map[key];
857         if (path_string === undefined) {
858             if (this.parent) {
859                 return this.parent.get_object(key, silent_error);
860             }
861             if (silent_error) { return void 'nooo'; }
862             return null;
863         }
864
865         var object_match = instance;
866         var path = path_string.split('.');
867         // ignore first section
868         for(var i=1; i<path.length; ++i) {
869             object_match = object_match[path[i]];
870
871             if (object_match === undefined) {
872                 if (silent_error) { return void 'noooooo'; }
873                 return null;
874             }
875         }
876         return object_match;
877     },
878     /**
879      * Checks if the registry contains an object mapping for this key.
880      *
881      * @param {String} key key to look for
882      */
883     contains: function (key) {
884         if (key === undefined) { return false; }
885         if (key in this.map) {
886             return true
887         }
888         if (this.parent) {
889             return this.parent.contains(key);
890         }
891         return false;
892     },
893     /**
894      * Tries a number of keys, and returns the first object matching one of
895      * the keys.
896      *
897      * @param {Array} keys a sequence of keys to fetch the object for
898      * @returns {Class} the first class found matching an object
899      */
900     get_any: function (keys) {
901         for (var i=0; i<keys.length; ++i) {
902             var key = keys[i];
903             if (!this.contains(key)) {
904                 continue;
905             }
906
907             return this.get_object(key);
908         }
909         return null;
910     },
911     /**
912      * Adds a new key and value to the registry.
913      *
914      * This method can be chained.
915      *
916      * @param {String} key
917      * @param {String} object_path fully qualified dotted object path
918      * @returns {instance.web.Registry} itself
919      */
920     add: function (key, object_path) {
921         this.map[key] = object_path;
922         return this;
923     },
924     /**
925      * Creates and returns a copy of the current mapping, with the provided
926      * mapping argument added in (replacing existing keys if needed)
927      *
928      * Parent and child remain linked, a new key in the parent (which is not
929      * overwritten by the child) will appear in the child.
930      *
931      * @param {Object} [mapping={}] a mapping of keys to object-paths
932      */
933     extend: function (mapping) {
934         var child = new instance.web.Registry(mapping);
935         child.parent = this;
936         return child;
937     },
938     /**
939      * @deprecated use Registry#extend
940      */
941     clone: function (mapping) {
942         console.warn('Registry#clone is deprecated, use Registry#extend');
943         return this.extend(mapping);
944     }
945 });
946
947 instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
948     triggers: {
949         'request': 'Request sent',
950         'response': 'Response received',
951         'response_failed': 'HTTP Error response or timeout received',
952         'error': 'The received response is an JSON-RPC error',
953     },
954     /**
955      * @constructs instance.web.JsonRPC
956      *
957      * @param {String} [server] JSON-RPC endpoint hostname
958      * @param {String} [port] JSON-RPC endpoint port
959      */
960     init: function() {
961         instance.web.PropertiesMixin.init.call(this);
962         this.server = null;
963         this.debug = ($.deparam($.param.querystring()).debug !== undefined);
964         this.override_session = false;
965         this.session_id = undefined;
966     },
967     setup: function(origin) {
968         var window_origin = location.protocol+"//"+location.host, self=this;
969         this.origin = origin ? _.str.rtrim(origin,'/') : window_origin;
970         this.prefix = this.origin;
971         this.server = this.origin; // keep chs happy
972         this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
973     },
974     /**
975      * Executes an RPC call, registering the provided callbacks.
976      *
977      * Registers a default error callback if none is provided, and handles
978      * setting the correct session id and session context in the parameter
979      * objects
980      *
981      * @param {String} url RPC endpoint
982      * @param {Object} params call parameters
983      * @param {Object} options additional options for rpc call
984      * @param {Function} success_callback function to execute on RPC call success
985      * @param {Function} error_callback function to execute on RPC call failure
986      * @returns {jQuery.Deferred} jquery-provided ajax deferred
987      */
988     rpc: function(url, params, options) {
989         var self = this;
990         options = options || {};
991         // url can be an $.ajax option object
992         if (_.isString(url)) {
993             url = { url: url };
994         }
995         _.defaults(params, {
996             context: this.user_context || {}
997         });
998         // Construct a JSON-RPC2 request, method is currently unused
999         var payload = {
1000             jsonrpc: '2.0',
1001             method: 'call',
1002             params: params,
1003             id: _.uniqueId('r')
1004         };
1005         var deferred = $.Deferred();
1006         if (! options.shadow)
1007             this.trigger('request', url, payload);
1008         
1009         if (options.timeout)
1010             url.timeout = options.timeout;
1011
1012         this.rpc_function(url, payload).then(
1013             function (response, textStatus, jqXHR) {
1014                 if (! options.shadow)
1015                     self.trigger('response', response);
1016                 if (!response.error) {
1017                     deferred.resolve(response["result"], textStatus, jqXHR);
1018                 } else if (response.error.code === 100) {
1019                     self.uid = false;
1020                 } else {
1021                     deferred.reject(response.error, $.Event());
1022                 }
1023             },
1024             function(jqXHR, textStatus, errorThrown) {
1025                 if (! options.shadow)
1026                     self.trigger('response_failed', jqXHR);
1027                 var error = {
1028                     code: -32098,
1029                     message: "XmlHttpRequestError " + errorThrown,
1030                     data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
1031                 };
1032                 deferred.reject(error, $.Event());
1033             });
1034         // Allow deferred user to disable rpc_error call in fail
1035         deferred.fail(function() {
1036             deferred.fail(function(error, event) {
1037                 if (!event.isDefaultPrevented()) {
1038                     self.trigger('error', error, event);
1039                 }
1040             });
1041         });
1042         return deferred;
1043     },
1044     /**
1045      * Raw JSON-RPC call
1046      *
1047      * @returns {jQuery.Deferred} ajax-webd deferred object
1048      */
1049     rpc_json: function(url, payload) {
1050         var self = this;
1051         var ajax = _.extend({
1052             type: "POST",
1053             dataType: 'json',
1054             contentType: 'application/json',
1055             data: JSON.stringify(payload),
1056             processData: false,
1057             headers: {
1058                 "X-Openerp-Session-Id": this.override_session ? this.session_id : undefined,
1059             },
1060         }, url);
1061         if (this.synch)
1062             ajax.async = false;
1063         return $.ajax(ajax);
1064     },
1065     rpc_jsonp: function(url, payload) {
1066         var self = this;
1067         // extracted from payload to set on the url
1068         var data = {
1069             session_id: this.session_id,
1070             id: payload.id,
1071         };
1072
1073         var set_sid = function (response, textStatus, jqXHR) {
1074             // If response give us the http session id, we store it for next requests...
1075             if (response.session_id) {
1076                 self.session_id = response.session_id;
1077             }
1078         };
1079
1080         url.url = this.url(url.url, null);
1081         var ajax = _.extend({
1082             type: "GET",
1083             dataType: 'jsonp',
1084             jsonp: 'jsonp',
1085             cache: false,
1086             data: data
1087         }, url);
1088         if (this.synch)
1089             ajax.async = false;
1090         var payload_str = JSON.stringify(payload);
1091         var payload_url = $.param({r:payload_str});
1092         if (payload_url.length < 2000) {
1093             // Direct jsonp request
1094             ajax.data.r = payload_str;
1095             return $.ajax(ajax).done(set_sid);
1096         } else {
1097             // Indirect jsonp request
1098             var ifid = _.uniqueId('oe_rpc_iframe');
1099             var display = self.debug ? 'block' : 'none';
1100             var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display));
1101             var $form = $('<form>')
1102                         .attr('method', 'POST')
1103                         .attr('target', ifid)
1104                         .attr('enctype', "multipart/form-data")
1105                         .attr('action', ajax.url + '?jsonp=1&' + $.param(data))
1106                         .append($('<input type="hidden" name="r" />').attr('value', payload_str))
1107                         .hide()
1108                         .appendTo($('body'));
1109             var cleanUp = function() {
1110                 if ($iframe) {
1111                     $iframe.unbind("load").remove();
1112                 }
1113                 $form.remove();
1114             };
1115             var deferred = $.Deferred();
1116             // the first bind is fired up when the iframe is added to the DOM
1117             $iframe.bind('load', function() {
1118                 // the second bind is fired up when the result of the form submission is received
1119                 $iframe.unbind('load').bind('load', function() {
1120                     $.ajax(ajax).always(function() {
1121                         cleanUp();
1122                     }).done(function() {
1123                         deferred.resolve.apply(deferred, arguments);
1124                     }).fail(function() {
1125                         deferred.reject.apply(deferred, arguments);
1126                     });
1127                 });
1128                 // now that the iframe can receive data, we fill and submit the form
1129                 $form.submit();
1130             });
1131             // append the iframe to the DOM (will trigger the first load)
1132             $form.after($iframe);
1133             return deferred.done(set_sid);
1134         }
1135     },
1136
1137     url: function(path, params) {
1138         params = _.extend(params || {});
1139         if (this.override_session)
1140             params.session_id = this.session_id;
1141         var qs = '?' + $.param(params);
1142         var prefix = _.any(['http://', 'https://', '//'], _.bind(_.str.startsWith, null, path)) ? '' : this.prefix; 
1143         return prefix + path + qs;
1144     },
1145 });
1146
1147 instance.web.py_eval = function(expr, context) {
1148     return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null}));
1149 };
1150
1151 }
1152
1153 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: