[FIX]tooltip: overwrite bootstrap default for tooltip
[odoo/odoo.git] / addons / web / static / src / js / chrome.js
1 /*---------------------------------------------------------
2  * OpenERP Web chrome
3  *---------------------------------------------------------*/
4 (function() {
5
6 var instance = openerp;
7 openerp.web.chrome = {};
8
9 var QWeb = instance.web.qweb,
10     _t = instance.web._t;
11
12 instance.web.Notification =  instance.web.Widget.extend({
13     template: 'Notification',
14     init: function() {
15         this._super.apply(this, arguments);
16         instance.web.notification = this;
17     },
18     start: function() {
19         this._super.apply(this, arguments);
20         this.$el.notify({
21             speed: 500,
22             expires: 2500
23         });
24     },
25     notify: function(title, text, sticky) {
26         sticky = !!sticky;
27         var opts = {};
28         if (sticky) {
29             opts.expires = false;
30         }
31         return this.$el.notify('create', {
32             title: title,
33             text: text
34         }, opts);
35     },
36     warn: function(title, text, sticky) {
37         sticky = !!sticky;
38         var opts = {};
39         if (sticky) {
40             opts.expires = false;
41         }
42         return this.$el.notify('create', 'oe_notification_alert', {
43             title: title,
44             text: text
45         }, opts);
46     }
47 });
48
49 instance.web.action_notify = function(element, action) {
50     element.do_notify(action.params.title, action.params.text, action.params.sticky);
51 };
52 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
53
54 instance.web.action_warn = function(element, action) {
55     element.do_warn(action.params.title, action.params.text, action.params.sticky);
56 };
57 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
58
59 /**
60     A useful class to handle dialogs.
61
62     Attributes:
63     - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
64     during the lifecycle of the dialog.
65 */
66 instance.web.Dialog = instance.web.Widget.extend({
67     dialog_title: "",
68     /**
69         Constructor.
70
71         @param {Widget} parent
72         @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
73             dictionary can contain the following keys:
74             - size: one of the following: 'large', 'medium', 'small'
75             - dialogClass: class to add to the body of dialog
76             - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
77                 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
78                 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
79                 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
80                 feature does not allow that kind of personalization.
81             - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
82         @param {jQuery object} content Some content to replace this.$el .
83     */
84     init: function (parent, options, content) {
85         var self = this;
86         this._super(parent);
87         this.content_to_set = content;
88         this.dialog_options = {
89             destroy_on_close: true,
90             size: 'large', //'medium', 'small'
91             buttons: null,
92         };
93         if (options) {
94             _.extend(this.dialog_options, options);
95         }
96         this.on("closing", this, this._closing);
97         this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
98     },
99     renderElement: function() {
100         if (this.content_to_set) {
101             this.setElement(this.content_to_set);
102         } else if (this.template) {
103             this._super();
104         }
105     },
106     /**
107         Opens the popup. Inits the dialog if it is not already inited.
108
109         @return this
110     */
111     open: function() {
112         if (!this.dialog_inited) {
113             this.init_dialog();
114         }
115         this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
116         return this;
117     },
118     _add_buttons: function(buttons) {
119         var self = this;
120         var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
121         _.each(buttons, function(fn, text) {
122             // buttons can be object or array
123             if (!_.isFunction(fn)) {
124                 text = fn.text;
125                 fn = fn.click;
126             }
127             var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
128             $customButons.append($but);
129             $but.on('click', function(ev) {
130                 fn.call(self.$el, ev);
131             });
132         });
133     },
134     /**
135         Initializes the popup.
136
137         @return The result returned by start().
138     */
139     init_dialog: function() {
140         var self = this;
141         var options = _.extend({}, this.dialog_options);
142         options.title = options.title || this.dialog_title;
143         if (options.buttons) {
144             this._add_buttons(options.buttons);
145             delete(options.buttons);
146         }
147         this.renderElement();
148
149         this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
150         this.$el.modal({
151             'backdrop': false,
152             'keyboard': true,
153         });
154         if (options.size !== 'large'){
155             var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg')
156             if (options.size === 'small'){
157                 dialog_class_size.addClass('modal-sm');
158             }
159         }
160
161         this.$el.appendTo(this.$dialog_box.find(".modal-body"));
162         var $dialog_content = this.$dialog_box.find('.modal-content');
163         if (options.dialogClass){
164             $dialog_content.find(".modal-body").addClass(options.dialogClass);
165         }
166         $dialog_content.openerpClass();
167
168         this.$dialog_box.on('hidden.bs.modal', this, function(){
169             self.trigger("closing");
170         });
171         this.$dialog_box.modal('show');
172
173         this.dialog_inited = true;
174         var res = this.start();
175         return res;
176     },
177     /**
178         Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
179     */
180     close: function(reason) {
181         if (this.dialog_inited && this.$el.is(":data(bs.modal)")) {
182             this.$el.parents('.modal').modal('hide');
183         }
184     },
185     _closing: function() {
186         if (this.__tmp_dialog_destroying)
187             return;
188         if (this.dialog_options.destroy_on_close) {
189             this.__tmp_dialog_closing = true;
190             this.destroy();
191             this.__tmp_dialog_closing = undefined;
192         }
193     },
194     /**
195         Destroys the popup, also closes it.
196     */
197     destroy: function (reason) {
198         this.$buttons.remove();
199         var self = this;
200         _.each(this.getChildren(), function(el) {
201             el.destroy();
202         });
203         if (! this.__tmp_dialog_closing) {
204             this.__tmp_dialog_destroying = true;
205             this.close(reason);
206             this.__tmp_dialog_destroying = undefined;
207         }
208         if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
209             //we need this to put the instruction to remove modal from DOM at the end
210             //of the queue, otherwise it might already have been removed before the modal-backdrop
211             //is removed when pressing escape key
212             var $parent = this.$el.parents('.modal');
213             setTimeout(function () {
214                 $parent.remove();
215             },0);
216         }
217         this._super();
218     }
219 });
220
221 instance.web.CrashManager = instance.web.Class.extend({
222     init: function() {
223         this.active = true;
224     },
225
226     rpc_error: function(error) {
227         if (!this.active) {
228             return;
229         }
230         var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
231         if (handler) {
232             new (handler)(this, error).display();
233             return;
234         }
235         if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
236             this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
237             return;
238         }
239         if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
240                 || error.data.exception_type === "access_error") {
241             this.show_warning(error);
242         } else {
243             this.show_error(error);
244         }
245     },
246     show_warning: function(error) {
247         if (!this.active) {
248             return;
249         }
250         if (error.data.exception_type === "except_osv") {
251             error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
252         }
253         new instance.web.Dialog(this, {
254             size: 'medium',
255             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
256             buttons: [
257                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
258             ],
259         }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
260     },
261     show_error: function(error) {
262         if (!this.active) {
263             return;
264         }
265         var buttons = {};
266         buttons[_t("Ok")] = function() {
267             this.parents('.modal').modal('hide');
268         };
269         new instance.web.Dialog(this, {
270             title: "OpenERP " + _.str.capitalize(error.type),
271             buttons: buttons
272         }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
273     },
274     show_message: function(exception) {
275         this.show_error({
276             type: _t("Client Error"),
277             message: exception,
278             data: {debug: ""}
279         });
280     },
281 });
282
283 /**
284     An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
285 */
286 instance.web.ExceptionHandler = {
287     /**
288         @param parent The parent.
289         @param error The error object as returned by the JSON-RPC implementation.
290     */
291     init: function(parent, error) {},
292     /**
293         Called to inform to display the widget, if necessary. A typical way would be to implement
294         this interface in a class extending instance.web.Dialog and simply display the dialog in this
295         method.
296     */
297     display: function() {},
298 };
299
300 /**
301     The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
302     instance.web.ExceptionHandler.
303 */
304 instance.web.crash_manager_registry = new instance.web.Registry();
305
306 /**
307  * Handle redirection warnings, which behave more or less like a regular
308  * warning, with an additional redirection button.
309  */
310 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
311     init: function(parent, error) {
312         this._super(parent);
313         this.error = error;
314     },
315     display: function() {
316         error = this.error;
317         error.data.message = error.data.arguments[0];
318
319         new instance.web.Dialog(this, {
320             size: 'medium',
321             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
322             buttons: [
323                 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }},
324                 {text: error.data.arguments[2], click: function() {
325                     window.location.href='#action='+error.data.arguments[1];
326                     this.$el.parents('.modal').modal('hide');
327                 }}
328             ],
329         }, QWeb.render('CrashManager.warning', {error: error})).open();
330         this.destroy();
331     }
332 });
333 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
334
335 instance.web.Loading = instance.web.Widget.extend({
336     template: _t("Loading"),
337     init: function(parent) {
338         this._super(parent);
339         this.count = 0;
340         this.blocked_ui = false;
341         this.session.on("request", this, this.request_call);
342         this.session.on("response", this, this.response_call);
343         this.session.on("response_failed", this, this.response_call);
344     },
345     destroy: function() {
346         this.on_rpc_event(-this.count);
347         this._super();
348     },
349     request_call: function() {
350         this.on_rpc_event(1);
351     },
352     response_call: function() {
353         this.on_rpc_event(-1);
354     },
355     on_rpc_event : function(increment) {
356         var self = this;
357         if (!this.count && increment === 1) {
358             // Block UI after 3s
359             this.long_running_timer = setTimeout(function () {
360                 self.blocked_ui = true;
361                 instance.web.blockUI();
362             }, 3000);
363         }
364
365         this.count += increment;
366         if (this.count > 0) {
367             if (instance.session.debug) {
368                 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
369             } else {
370                 this.$el.text(_t("Loading"));
371             }
372             this.$el.show();
373             this.getParent().$el.addClass('oe_wait');
374         } else {
375             this.count = 0;
376             clearTimeout(this.long_running_timer);
377             // Don't unblock if blocked by somebody else
378             if (self.blocked_ui) {
379                 this.blocked_ui = false;
380                 instance.web.unblockUI();
381             }
382             this.$el.fadeOut();
383             this.getParent().$el.removeClass('oe_wait');
384         }
385     }
386 });
387
388 instance.web.DatabaseManager = instance.web.Widget.extend({
389     init: function(parent) {
390         this._super(parent);
391         this.unblockUIFunction = instance.web.unblockUI;
392         $.validator.addMethod('matches', function (s, _, re) {
393             return new RegExp(re).test(s);
394         }, _t("Invalid database name"));
395     },
396     start: function() {
397         var self = this;
398         $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
399         var fetch_db = this.rpc("/web/database/get_list", {}).then(
400             function(result) {
401                 self.db_list = result;
402             },
403             function (_, ev) {
404                 ev.preventDefault();
405                 self.db_list = null;
406             });
407         var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
408             self.lang_list = result;
409         });
410         return $.when(fetch_db, fetch_langs).always(self.do_render);
411     },
412     do_render: function() {
413         var self = this;
414         instance.webclient.toggle_bars(true);
415         self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
416         $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
417         $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
418         $('ul.oe_secondary_submenu > li:first').addClass('active');
419         $('ul.oe_secondary_submenu > li').bind('click', function (event) {
420             var menuitem = $(this);
421             menuitem.addClass('active').siblings().removeClass('active');
422             var form_id =menuitem.find('a').attr('href');
423             $(form_id).show().siblings().hide();
424             event.preventDefault();
425         });
426         $('#back-to-login').click(self.do_exit);
427         self.$el.find("td").addClass("oe_form_group_cell");
428         self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
429         self.$el.find("label").addClass("oe_form_label");
430         self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
431         self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
432         self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
433         self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
434         self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
435         self.$el.find("form[name=change_pwd_form]").validate({
436             messages: {
437                 old_pwd: _t("Please enter your previous password"),
438                 new_pwd: _t("Please enter your new password"),
439                 confirm_pwd: {
440                     required: _t("Please confirm your new password"),
441                     equalTo: _t("The confirmation does not match the password")
442                 }
443             },
444             submitHandler: self.do_change_password
445         });
446     },
447     destroy: function () {
448         this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
449         this._super();
450     },
451     /**
452      * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
453      * from unblocking the UI
454      */
455     blockUI: function () {
456         instance.web.blockUI();
457         instance.web.unblockUI = function () {};
458     },
459     /**
460      * Reinstates $.unblockUI so third parties can play with blockUI, and
461      * unblocks the UI
462      */
463     unblockUI: function () {
464         instance.web.unblockUI = this.unblockUIFunction;
465         instance.web.unblockUI();
466     },
467     /**
468      * Displays an error dialog resulting from the various RPC communications
469      * failing over themselves
470      *
471      * @param {Object} error error description
472      * @param {String} error.title title of the error dialog
473      * @param {String} error.error message of the error dialog
474      */
475     display_error: function (error) {
476         return new instance.web.Dialog(this, {
477             size: 'medium',
478             title: error.title,
479             buttons: [
480                 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }}
481             ]
482         }, $('<div>').html(error.error)).open();
483     },
484     do_create: function(form) {
485         var self = this;
486         var fields = $(form).serializeArray();
487         self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
488             if (result) {
489                 instance.web.redirect('/web');
490             } else {
491                 alert("Failed to create database");
492             }
493         });
494     },
495     do_duplicate: function(form) {
496         var self = this;
497         var fields = $(form).serializeArray();
498         self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
499             if (result.error) {
500                 self.display_error(result);
501                 return;
502             }
503             self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
504             self.start();
505         });
506     },
507     do_drop: function(form) {
508         var self = this;
509         var $form = $(form),
510             fields = $form.serializeArray(),
511             $db_list = $form.find('[name=drop_db]'),
512             db = $db_list.val();
513         if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
514             return;
515         }
516         self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
517             if (result.error) {
518                 self.display_error(result);
519                 return;
520             }
521             self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
522             self.start();
523         });
524     },
525     do_backup: function(form) {
526         var self = this;
527         self.blockUI();
528         self.session.get_file({
529             form: form,
530             success: function () {
531                 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
532             },
533             error: function(error){
534                if(error){
535                   self.display_error({
536                         title: _t("Backup Database"),
537                         error: 'AccessDenied'
538                   });
539                }
540             },
541             complete: function() {
542                 self.unblockUI();
543             }
544         });
545     },
546     do_restore: function(form) {
547         var self = this;
548         self.blockUI();
549         $(form).ajaxSubmit({
550             url: '/web/database/restore',
551             type: 'POST',
552             resetForm: true,
553             success: function (body) {
554                 // If empty body, everything went fine
555                 if (!body) { return; }
556
557                 if (body.indexOf('403 Forbidden') !== -1) {
558                     self.display_error({
559                         title: _t("Access Denied"),
560                         error: _t("Incorrect super-administrator password")
561                     });
562                 } else {
563                     self.display_error({
564                         title: _t("Restore Database"),
565                         error: _t("Could not restore the database")
566                     });
567                 }
568             },
569             complete: function() {
570                 self.unblockUI();
571                 self.do_notify(_t("Restored"), _t("Database restored successfully"));
572             }
573         });
574     },
575     do_change_password: function(form) {
576         var self = this;
577         self.rpc("/web/database/change_password", {
578             'fields': $(form).serializeArray()
579         }).done(function(result) {
580             if (result.error) {
581                 self.display_error(result);
582                 return;
583             }
584             self.unblockUI();
585             self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
586         });
587     },
588     do_exit: function () {
589         this.$el.remove();
590         instance.web.redirect('/web');
591     }
592 });
593 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
594
595 instance.web.login = function() {
596     instance.web.redirect('/web/login');
597 };
598 instance.web.client_actions.add("login", "instance.web.login");
599
600 instance.web.logout = function() {
601     instance.web.redirect('/web/session/logout');
602 };
603 instance.web.client_actions.add("logout", "instance.web.logout");
604
605
606 /**
607  * Redirect to url by replacing window.location
608  * If wait is true, sleep 1s and wait for the server i.e. after a restart.
609  */
610 instance.web.redirect = function(url, wait) {
611     // Dont display a dialog if some xmlhttprequest are in progress
612     if (instance.client && instance.client.crashmanager) {
613         instance.client.crashmanager.active = false;
614     }
615
616     var load = function() {
617         var old = "" + window.location;
618         var old_no_hash = old.split("#")[0];
619         var url_no_hash = url.split("#")[0];
620         location.assign(url);
621         if (old_no_hash === url_no_hash) {
622             location.reload(true);
623         }
624     };
625
626     var wait_server = function() {
627         instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
628             setTimeout(wait_server, 250);
629         });
630     };
631
632     if (wait) {
633         setTimeout(wait_server, 1000);
634     } else {
635         load();
636     }
637 };
638
639 /**
640  * Client action to reload the whole interface.
641  * If params.menu_id, it opens the given menu entry.
642  * If params.wait, reload will wait the openerp server to be reachable before reloading
643  */
644 instance.web.Reload = function(parent, action) {
645     var params = action.params || {};
646     var menu_id = params.menu_id || false;
647     var l = window.location;
648
649     var sobj = $.deparam(l.search.substr(1));
650     if (params.url_search) {
651         sobj = _.extend(sobj, params.url_search);
652     }
653     var search = '?' + $.param(sobj);
654
655     var hash = l.hash;
656     if (menu_id) {
657         hash = "#menu_id=" + menu_id;
658     }
659     var url = l.protocol + "//" + l.host + l.pathname + search + hash;
660
661     instance.web.redirect(url, params.wait);
662 };
663 instance.web.client_actions.add("reload", "instance.web.Reload");
664
665 /**
666  * Client action to go back in breadcrumb history.
667  * If can't go back in history stack, will go back to home.
668  */
669 instance.web.HistoryBack = function(parent) {
670     if (!parent.history_back()) {
671         instance.web.Home(parent);
672     }
673 };
674 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
675
676 /**
677  * Client action to go back home.
678  */
679 instance.web.Home = function(parent, action) {
680     var url = '/' + (window.location.search || '');
681     instance.web.redirect(url, action && action.params && action.params.wait);
682 };
683 instance.web.client_actions.add("home", "instance.web.Home");
684
685 instance.web.ChangePassword =  instance.web.Widget.extend({
686     template: "ChangePassword",
687     start: function() {
688         var self = this;
689         this.getParent().dialog_title = _t("Change Password");
690         var $button = self.$el.find('.oe_form_button');
691         $button.appendTo(this.getParent().$buttons);
692         $button.eq(2).click(function(){
693            self.getParent().close();
694         });
695         $button.eq(0).click(function(){
696           self.rpc("/web/session/change_password",{
697                'fields': $("form[name=change_password_form]").serializeArray()
698           }).done(function(result) {
699                if (result.error) {
700                   self.display_error(result);
701                   return;
702                } else {
703                    instance.webclient.on_logout();
704                }
705           });
706        });
707     },
708     display_error: function (error) {
709         return new instance.web.Dialog(this, {
710             size: 'medium',
711             title: error.title,
712             buttons: [
713                 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }}
714             ]
715         }, $('<div>').html(error.error)).open();
716     },
717 });
718 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
719
720 instance.web.Menu =  instance.web.Widget.extend({
721     template: 'Menu',
722     init: function() {
723         var self = this;
724         this._super.apply(this, arguments);
725         this.has_been_loaded = $.Deferred();
726         this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
727         this.data = {data:{children:[]}};
728         this.on("menu_loaded", this, function (menu_data) {
729             self.reflow();
730             // launch the fetch of needaction counters, asynchronous
731             if (!_.isEmpty(menu_data.all_menu_ids)) {
732                 this.do_load_needaction(menu_data.all_menu_ids);
733             }
734         });
735         var lazyreflow = _.debounce(this.reflow.bind(this), 200);
736         instance.web.bus.on('resize', this, function() {
737             self.$el.height(0);
738             lazyreflow();
739         });
740     },
741     start: function() {
742         this._super.apply(this, arguments);
743         this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
744         this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
745         return this.do_reload();
746     },
747     do_reload: function() {
748         var self = this;
749         return this.rpc("/web/menu/load", {}).done(function(r) {
750             self.menu_loaded(r);
751         });
752     },
753     menu_loaded: function(data) {
754         var self = this;
755         this.data = {data: data};
756         this.renderElement();
757         this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
758         this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
759         // Hide second level submenus
760         this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
761         if (self.current_menu) {
762             self.open_menu(self.current_menu);
763         }
764         this.trigger('menu_loaded', data);
765         this.has_been_loaded.resolve();
766     },
767     do_load_needaction: function (menu_ids) {
768         var self = this;
769         menu_ids = _.compact(menu_ids);
770         if (_.isEmpty(menu_ids)) {
771             return $.when();
772         }
773         return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
774             self.on_needaction_loaded(r);
775         });
776     },
777     on_needaction_loaded: function(data) {
778         var self = this;
779         this.needaction_data = data;
780         _.each(this.needaction_data, function (item, menu_id) {
781             var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
782             $item.find('.badge').remove();
783             if (item.needaction_counter && item.needaction_counter > 0) {
784                 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
785             }
786         });
787     },
788     /**
789      * Reflow the menu items and dock overflowing items into a "More" menu item.
790      * Automatically called when 'menu_loaded' event is triggered and on window resizing.
791      */
792     reflow: function() {
793         var self = this;
794         this.$el.height('auto').show();
795         var $more_container = this.$('#menu_more_container').hide();
796         var $more = this.$('#menu_more');
797         $more.children('li').insertBefore($more_container);
798         var $toplevel_items = this.$el.children('li').not($more_container).hide();
799         $toplevel_items.each(function() {
800             var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
801             self.$el.parent().children(':visible').each(function() {
802                 remaining_space -= $(this).outerWidth();
803             });
804             if ($(this).width() > remaining_space) {
805                 return false;
806             }
807             $(this).show();
808         });
809         $more.append($toplevel_items.filter(':hidden').show());
810         $more_container.toggle(!!$more.children().length);
811         // Hide toplevel item if there is only one
812         var $toplevel = this.$el.children("li:visible");
813         if ($toplevel.length === 1) {
814             $toplevel.hide();
815         }
816     },
817     /**
818      * Opens a given menu by id, as if a user had browsed to that menu by hand
819      * except does not trigger any event on the way
820      *
821      * @param {Number} id database id of the terminal menu to select
822      */
823     open_menu: function (id) {
824         this.current_menu = id;
825         this.session.active_id = id;
826         var $clicked_menu, $sub_menu, $main_menu;
827         $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
828         this.trigger('open_menu', id, $clicked_menu);
829
830         if (this.$secondary_menus.has($clicked_menu).length) {
831             $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
832             $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
833         } else {
834             $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
835             $main_menu = $clicked_menu;
836         }
837
838         // Activate current main menu
839         this.$el.find('.active').removeClass('active');
840         $main_menu.addClass('active');
841
842         // Show current sub menu
843         this.$secondary_menus.find('.oe_secondary_menu').hide();
844         $sub_menu.show();
845
846         // Hide/Show the leftbar menu depending of the presence of sub-items
847         this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
848
849         // Activate current menu item and show parents
850         this.$secondary_menus.find('.active').removeClass('active');
851         if ($main_menu !== $clicked_menu) {
852             $clicked_menu.parents().show();
853             if ($clicked_menu.is('.oe_menu_toggler')) {
854                 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
855             } else {
856                 $clicked_menu.parent().addClass('active');
857             }
858         }
859     },
860     /**
861      * Call open_menu with the first menu_item matching an action_id
862      *
863      * @param {Number} id the action_id to match
864      */
865     open_action: function (id) {
866         var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
867         var menu_id = $menu.data('menu');
868         if (menu_id) {
869             this.open_menu(menu_id);
870         }
871     },
872     /**
873      * Process a click on a menu item
874      *
875      * @param {Number} id the menu_id
876      * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
877      */
878     menu_click: function(id, needaction) {
879         if (!id) { return; }
880
881         // find back the menuitem in dom to get the action
882         var $item = this.$el.find('a[data-menu=' + id + ']');
883         if (!$item.length) {
884             $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
885         }
886         var action_id = $item.data('action-id');
887         // If first level menu doesnt have action trigger first leaf
888         if (!action_id) {
889             if(this.$el.has($item).length) {
890                 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
891                 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
892                 if($items.length) {
893                     action_id = $items.data('action-id');
894                     id = $items.data('menu');
895                 }
896             }
897         }
898         if (action_id) {
899             this.trigger('menu_click', {
900                 action_id: action_id,
901                 needaction: needaction,
902                 id: id,
903                 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
904             }, $item);
905         }
906         this.open_menu(id);
907     },
908     do_reload_needaction: function () {
909         var self = this;
910         if (self.current_menu) {
911             self.do_load_needaction([self.current_menu]).then(function () {
912                 self.trigger("need_action_reloaded");
913             });
914         }
915     },
916     /**
917      * Jquery event handler for menu click
918      *
919      * @param {Event} ev the jquery event
920      */
921     on_top_menu_click: function(ev) {
922         var self = this;
923         var id = $(ev.currentTarget).data('menu');
924         var menu_ids = [id];
925         var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
926         function add_menu_ids (menu) {
927             if (menu.children) {
928                 _.each(menu.children, function (menu) {
929                     menu_ids.push(menu.id);
930                     add_menu_ids(menu);
931                 });
932             }
933         }
934         add_menu_ids(menu);
935         self.do_load_needaction(menu_ids).then(function () {
936             self.trigger("need_action_reloaded");
937         });
938         this.on_menu_click(ev);
939     },
940     on_menu_click: function(ev) {
941         ev.preventDefault();
942         var needaction = $(ev.target).is('div#menu_counter');
943         this.menu_click($(ev.currentTarget).data('menu'), needaction);
944     },
945 });
946
947 instance.web.UserMenu =  instance.web.Widget.extend({
948     template: "UserMenu",
949     init: function(parent) {
950         this._super(parent);
951         this.update_promise = $.Deferred().resolve();
952     },
953     start: function() {
954         var self = this;
955         this._super.apply(this, arguments);
956         this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
957             ev.preventDefault();
958             var f = self['on_menu_' + $(this).data('menu')];
959             if (f) {
960                 f($(this));
961             }
962         });
963     },
964     do_update: function () {
965         var self = this;
966         var fct = function() {
967             var $avatar = self.$el.find('.oe_topbar_avatar');
968             $avatar.attr('src', $avatar.data('default-src'));
969             if (!self.session.uid)
970                 return;
971             var func = new instance.web.Model("res.users").get_func("read");
972             return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
973                 var topbar_name = res.name;
974                 if(instance.session.debug)
975                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
976                 if(res.company_id[0] > 1)
977                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
978                 self.$el.find('.oe_topbar_name').text(topbar_name);
979                 if (!instance.session.debug) {
980                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
981                 }
982                 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
983                 $avatar.attr('src', avatar_src);
984             });
985         };
986         this.update_promise = this.update_promise.then(fct, fct);
987     },
988     on_menu_help: function() {
989         window.open('http://help.openerp.com', '_blank');
990     },
991     on_menu_logout: function() {
992         this.trigger('user_logout');
993     },
994     on_menu_settings: function() {
995         var self = this;
996         if (!this.getParent().has_uncommitted_changes()) {
997             self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
998                 result.res_id = instance.session.uid;
999                 self.getParent().action_manager.do_action(result);
1000             });
1001         }
1002     },
1003     on_menu_account: function() {
1004         var self = this;
1005         if (!this.getParent().has_uncommitted_changes()) {
1006             var P = new instance.web.Model('ir.config_parameter');
1007             P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1008                 var state = {
1009                             'd': instance.session.db,
1010                             'u': window.location.protocol + '//' + window.location.host,
1011                         };
1012                 var params = {
1013                     response_type: 'token',
1014                     client_id: dbuuid || '',
1015                     state: JSON.stringify(state),
1016                     scope: 'userinfo',
1017                 };
1018                 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1019             });
1020         }
1021     },
1022     on_menu_about: function() {
1023         var self = this;
1024         self.rpc("/web/webclient/version_info", {}).done(function(res) {
1025             var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1026             $help.find('a.oe_activate_debug_mode').click(function (e) {
1027                 e.preventDefault();
1028                 window.location = $.param.querystring( window.location.href, 'debug');
1029             });
1030             new instance.web.Dialog(this, {
1031                 size: 'medium',
1032                 dialogClass: 'oe_act_window',
1033                 title: _t("About"),
1034             }, $help).open();
1035         });
1036     },
1037 });
1038
1039 instance.web.FullscreenWidget = instance.web.Widget.extend({
1040     /**
1041      * Widgets extending the FullscreenWidget will be displayed fullscreen,
1042      * and will have a fixed 1:1 zoom level on mobile devices.
1043      */
1044     start: function(){
1045         if(!$('#oe-fullscreenwidget-viewport').length){
1046             $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1047         }
1048         instance.webclient.set_content_full_screen(true);
1049         return this._super();
1050     },
1051     destroy: function(){
1052         instance.webclient.set_content_full_screen(false);
1053         $('#oe-fullscreenwidget-viewport').remove();
1054         return this._super();
1055     },
1056
1057 });
1058
1059 instance.web.Client = instance.web.Widget.extend({
1060     init: function(parent, origin) {
1061         instance.client = instance.webclient = this;
1062         this.client_options = {};
1063         this._super(parent);
1064         this.origin = origin;
1065     },
1066     start: function() {
1067         var self = this;
1068         return instance.session.session_bind(this.origin).then(function() {
1069             var $e = $(QWeb.render(self._template, {widget: self}));
1070             self.replaceElement($e);
1071             $e.openerpClass();
1072             self.bind_events();
1073             return self.show_common();
1074         });
1075     },
1076     bind_events: function() {
1077         var self = this;
1078         this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1079             $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1080         });
1081         this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1082             ev.preventDefault();
1083             var $toggle = $(this);
1084             var doc_width = $(document).width();
1085             var $menu = $toggle.siblings('.oe_dropdown_menu');
1086             $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1087             var state = $menu.is('.oe_opened');
1088             setTimeout(function() {
1089                 // Do not alter propagation
1090                 $toggle.add($menu).toggleClass('oe_opened', !state);
1091                 if (!state) {
1092                     // Move $menu if outside window's edge
1093                     var offset = $menu.offset();
1094                     var menu_width = $menu.width();
1095                     var x = doc_width - offset.left - menu_width - 2;
1096                     if (x < 0) {
1097                         $menu.offset({ left: offset.left + x }).width(menu_width);
1098                     }
1099                 }
1100             }, 0);
1101         });
1102         instance.web.bus.on('click', this, function(ev) {
1103             $.fn.tooltip('destroy');
1104             if (!$(ev.target).is('input[type=file]')) {
1105                 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1106             }
1107         });
1108     },
1109     show_common: function() {
1110         var self = this;
1111         this.crashmanager =  new instance.web.CrashManager();
1112         instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1113         self.notification = new instance.web.Notification(this);
1114         self.notification.appendTo(self.$el);
1115         self.loading = new instance.web.Loading(self);
1116         self.loading.appendTo(self.$el);
1117         self.action_manager = new instance.web.ActionManager(self);
1118         self.action_manager.appendTo(self.$('.oe_application'));
1119     },
1120     toggle_bars: function(value) {
1121         this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1122     },
1123     has_uncommitted_changes: function() {
1124         return false;
1125     },
1126 });
1127
1128 instance.web.WebClient = instance.web.Client.extend({
1129     _template: 'WebClient',
1130     events: {
1131         'click .oe_logo_edit_admin': 'logo_edit'
1132     },
1133     init: function(parent, client_options) {
1134         this._super(parent);
1135         if (client_options) {
1136             _.extend(this.client_options, client_options);
1137         }
1138         this._current_state = null;
1139         this.menu_dm = new instance.web.DropMisordered();
1140         this.action_mutex = new $.Mutex();
1141         this.set('title_part', {"zopenerp": "OpenERP"});
1142     },
1143     start: function() {
1144         var self = this;
1145         this.on("change:title_part", this, this._title_changed);
1146         this._title_changed();
1147         return $.when(this._super()).then(function() {
1148             if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1149                 self.to_kitten();
1150             }
1151             if (self.session.session_is_valid()) {
1152                 self.show_application();
1153             }
1154             if (self.client_options.action) {
1155                 self.action_manager.do_action(self.client_options.action);
1156                 delete(self.client_options.action);
1157             }
1158         });
1159     },
1160     to_kitten: function() {
1161         this.kitten = true;
1162         $("body").addClass("kitten-mode-activated");
1163         $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1164         if ($.blockUI) {
1165             var imgkit = Math.floor(Math.random() * 2 + 1);
1166             $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1167         }
1168     },
1169     /**
1170         Sets the first part of the title of the window, dedicated to the current action.
1171     */
1172     set_title: function(title) {
1173         this.set_title_part("action", title);
1174     },
1175     /**
1176         Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1177         a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1178         title of the window separated by '-'.
1179     */
1180     set_title_part: function(part, title) {
1181         var tmp = _.clone(this.get("title_part"));
1182         tmp[part] = title;
1183         this.set("title_part", tmp);
1184     },
1185     _title_changed: function() {
1186         var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1187         var tmp = "";
1188         _.each(parts, function(part) {
1189             var str = this.get("title_part")[part];
1190             if (str) {
1191                 tmp = tmp ? tmp + " - " + str : str;
1192             }
1193         }, this);
1194         document.title = tmp;
1195     },
1196     show_common: function() {
1197         var self = this;
1198         this._super();
1199         window.onerror = function (message, file, line) {
1200             self.crashmanager.show_error({
1201                 type: _t("Client Error"),
1202                 message: message,
1203                 data: {debug: file + ':' + line}
1204             });
1205         };
1206     },
1207     show_application: function() {
1208         var self = this;
1209         self.toggle_bars(true);
1210         self.update_logo();
1211         self.menu = new instance.web.Menu(self);
1212         self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1213         self.menu.on('menu_click', this, this.on_menu_action);
1214         self.user_menu = new instance.web.UserMenu(self);
1215         self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1216         self.user_menu.on('user_logout', self, self.on_logout);
1217         self.user_menu.do_update();
1218         self.bind_hashchange();
1219         self.set_title();
1220         self.check_timezone();
1221         if (self.client_options.action_post_login) {
1222             self.action_manager.do_action(self.client_options.action_post_login);
1223             delete(self.client_options.action_post_login);
1224         }
1225     },
1226     update_logo: function() {
1227         var img = this.session.url('/web/binary/company_logo');
1228         this.$('.oe_logo img').attr('src', '').attr('src', img);
1229         this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1230     },
1231     logo_edit: function(ev) {
1232         var self = this;
1233         self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1234             self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1235                 result.res_id = res['company_id'][0];
1236                 result.target = "new";
1237                 result.views = [[false, 'form']];
1238                 result.flags = {
1239                     action_buttons: true,
1240                 };
1241                 self.action_manager.do_action(result);
1242                 var form = self.action_manager.dialog_widget.views.form.controller;
1243                 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1244                 form.on('record_saved', self, function() {
1245                     self.action_manager.dialog_stop();
1246                     self.update_logo();
1247                 });
1248             });
1249         });
1250         return false;
1251     },
1252     check_timezone: function() {
1253         var self = this;
1254         return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1255             var user_offset = result[0]['tz_offset'];
1256             var offset = -(new Date().getTimezoneOffset());
1257             // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1258             var browser_offset = (offset < 0) ? "-" : "+";
1259             browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1260             browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1261             if (browser_offset !== user_offset) {
1262                 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1263                 $icon.on('click', function() {
1264                     var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1265                         user_timezone: instance.session.user_context.tz || 'UTC',
1266                         user_offset: user_offset,
1267                         browser_offset: browser_offset,
1268                     }), true);
1269                     notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1270                         notification.close();
1271                     }).find('a').on('click', function() {
1272                         notification.close();
1273                         self.user_menu.on_menu_settings();
1274                         return false;
1275                     });
1276                 });
1277                 $icon.appendTo(self.$('.oe_systray'));
1278             }
1279         });
1280     },
1281     destroy_content: function() {
1282         _.each(_.clone(this.getChildren()), function(el) {
1283             el.destroy();
1284         });
1285         this.$el.children().remove();
1286     },
1287     do_reload: function() {
1288         var self = this;
1289         return this.session.session_reload().then(function () {
1290             instance.session.load_modules(true).then(
1291                 self.menu.proxy('do_reload')); });
1292
1293     },
1294     do_notify: function() {
1295         var n = this.notification;
1296         return n.notify.apply(n, arguments);
1297     },
1298     do_warn: function() {
1299         var n = this.notification;
1300         return n.warn.apply(n, arguments);
1301     },
1302     on_logout: function() {
1303         var self = this;
1304         if (!this.has_uncommitted_changes()) {
1305             self.action_manager.do_action('logout');
1306         }
1307     },
1308     bind_hashchange: function() {
1309         var self = this;
1310         $(window).bind('hashchange', this.on_hashchange);
1311
1312         var state = $.bbq.getState(true);
1313         if (_.isEmpty(state) || state.action == "login") {
1314             self.menu.has_been_loaded.done(function() {
1315                 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1316                     if(data.action_id) {
1317                         self.action_manager.do_action(data.action_id[0]);
1318                         self.menu.open_action(data.action_id[0]);
1319                     } else {
1320                         var first_menu_id = self.menu.$el.find("a:first").data("menu");
1321                         if(first_menu_id)
1322                             self.menu.menu_click(first_menu_id);
1323                     }
1324                 });
1325             });
1326         } else {
1327             $(window).trigger('hashchange');
1328         }
1329     },
1330     on_hashchange: function(event) {
1331         var self = this;
1332         var stringstate = event.getState(false);
1333         if (!_.isEqual(this._current_state, stringstate)) {
1334             var state = event.getState(true);
1335             if(!state.action && state.menu_id) {
1336                 self.menu.has_been_loaded.done(function() {
1337                     self.menu.do_reload().done(function() {
1338                         self.menu.menu_click(state.menu_id);
1339                     });
1340                 });
1341             } else {
1342                 state._push_me = false;  // no need to push state back...
1343                 this.action_manager.do_load_state(state, !!this._current_state);
1344             }
1345         }
1346         this._current_state = stringstate;
1347     },
1348     do_push_state: function(state) {
1349         this.set_title(state.title);
1350         delete state.title;
1351         var url = '#' + $.param(state);
1352         this._current_state = $.deparam($.param(state), false);     // stringify all values
1353         $.bbq.pushState(url);
1354         this.trigger('state_pushed', state);
1355     },
1356     on_menu_action: function(options) {
1357         var self = this;
1358         return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1359             .then(function (result) {
1360                 return self.action_mutex.exec(function() {
1361                     if (options.needaction) {
1362                         result.context = new instance.web.CompoundContext(result.context, {
1363                             search_default_message_unread: true,
1364                             search_disable_custom_filters: true,
1365                         });
1366                     }
1367                     var completed = $.Deferred();
1368                     $.when(self.action_manager.do_action(result, {
1369                         clear_breadcrumbs: true,
1370                         action_menu_id: self.menu.current_menu,
1371                     })).fail(function() {
1372                         self.menu.open_menu(options.previous_menu_id);
1373                     }).always(function() {
1374                         completed.resolve();
1375                     });
1376                     setTimeout(function() {
1377                         completed.resolve();
1378                     }, 2000);
1379                     // We block the menu when clicking on an element until the action has correctly finished
1380                     // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1381                     return completed;
1382                 });
1383             });
1384     },
1385     set_content_full_screen: function(fullscreen) {
1386         $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1387         this.$('.oe_webclient').toggleClass(
1388             'oe_content_full_screen', fullscreen);
1389     },
1390     has_uncommitted_changes: function() {
1391         var $e = $.Event('clear_uncommitted_changes');
1392         instance.web.bus.trigger('clear_uncommitted_changes', $e);
1393         if ($e.isDefaultPrevented()) {
1394             return true;
1395         } else {
1396             return this._super.apply(this, arguments);
1397         }
1398     },
1399 });
1400
1401 instance.web.EmbeddedClient = instance.web.Client.extend({
1402     _template: 'EmbedClient',
1403     init: function(parent, origin, dbname, login, key, action_id, options) {
1404         this._super(parent, origin);
1405         this.bind_credentials(dbname, login, key);
1406         this.action_id = action_id;
1407         this.options = options || {};
1408     },
1409     start: function() {
1410         var self = this;
1411         return $.when(this._super()).then(function() {
1412             return self.authenticate().then(function() {
1413                 if (!self.action_id) {
1414                     return;
1415                 }
1416                 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1417                     var action = result;
1418                     action.flags = _.extend({
1419                         //views_switcher : false,
1420                         search_view : false,
1421                         action_buttons : false,
1422                         sidebar : false
1423                         //pager : false
1424                     }, self.options, action.flags || {});
1425
1426                     self.do_action(action);
1427                 });
1428             });
1429         });
1430     },
1431
1432     do_action: function(/*...*/) {
1433         var am = this.action_manager;
1434         return am.do_action.apply(am, arguments);
1435     },
1436
1437     authenticate: function() {
1438         var s = instance.session;
1439         if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1440             return $.when();
1441         }
1442         return instance.session.session_authenticate(this.dbname, this.login, this.key);
1443     },
1444
1445     bind_credentials: function(dbname, login, key) {
1446         this.dbname = dbname;
1447         this.login = login;
1448         this.key = key;
1449     },
1450
1451 });
1452
1453 instance.web.embed = function (origin, dbname, login, key, action, options) {
1454     $('head').append($('<link>', {
1455         'rel': 'stylesheet',
1456         'type': 'text/css',
1457         'href': origin +'/web/webclient/css'
1458     }));
1459     var currentScript = document.currentScript;
1460     if (!currentScript) {
1461         var sc = document.getElementsByTagName('script');
1462         currentScript = sc[sc.length-1];
1463     }
1464     var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1465     client.insertAfter(currentScript);
1466 };
1467
1468 })();
1469
1470 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: