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