4 if (typeof(console) === "undefined") {
5 // Even IE9 only exposes console object if debug window opened
7 ('log error debug info warn assert clear dir dirxml trace group'
8 + ' groupCollapsed groupEnd time timeEnd profile profileEnd count'
9 + ' exception').split(/\s+/).forEach(function(property) {
10 console[property] = _.identity;
14 var instance = openerp;
15 openerp.web.core = {};
17 var ControllerMixin = {
19 * Informs the action manager to do an action. This supposes that
20 * the action manager can be found amongst the ancestors of the current widget.
21 * If that's not the case this method will simply return `false`.
23 do_action: function() {
24 var parent = this.getParent();
26 return parent.do_action.apply(parent, arguments);
30 do_notify: function() {
31 if (this.getParent()) {
32 return this.getParent().do_notify.apply(this,arguments);
37 if (this.getParent()) {
38 return this.getParent().do_warn.apply(this,arguments);
42 rpc: function(url, data, options) {
43 return this.alive(openerp.session.rpc(url, data, options));
48 A class containing common utility methods useful when working with OpenERP as well as the PropertiesMixin.
50 openerp.web.Controller = openerp.web.Class.extend(openerp.web.PropertiesMixin, ControllerMixin, {
52 * Constructs the object and sets its parent if a parent is given.
54 * @param {openerp.web.Controller} parent Binds the current instance to the given Controller instance.
55 * When that controller is destroyed by calling destroy(), the current instance will be
56 * destroyed too. Can be null.
58 init: function(parent) {
59 openerp.web.PropertiesMixin.init.call(this);
60 this.setParent(parent);
61 this.session = openerp.session;
65 openerp.web.Widget.include(_.extend({}, ControllerMixin, {
67 this._super.apply(this, arguments);
68 this.session = openerp.session;
72 instance.web.Registry = instance.web.Class.extend({
74 * Stores a mapping of arbitrary key (strings) to object paths (as strings
77 * Resolves those paths at query time in order to always fetch the correct
78 * object, even if those objects have been overloaded/replaced after the
79 * registry was created.
81 * An object path is simply a dotted name from the instance root to the
82 * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP
85 * @constructs instance.web.Registry
86 * @param {Object} mapping a mapping of keys to object-paths
88 init: function (mapping) {
90 this.map = mapping || {};
93 * Retrieves the object matching the provided key string.
95 * @param {String} key the key to fetch the object for
96 * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
97 * @returns {Class} the stored class, to initialize or null if not found
99 get_object: function (key, silent_error) {
100 var path_string = this.map[key];
101 if (path_string === undefined) {
103 return this.parent.get_object(key, silent_error);
105 if (silent_error) { return void 'nooo'; }
109 var object_match = instance;
110 var path = path_string.split('.');
111 // ignore first section
112 for(var i=1; i<path.length; ++i) {
113 object_match = object_match[path[i]];
115 if (object_match === undefined) {
116 if (silent_error) { return void 'noooooo'; }
123 * Checks if the registry contains an object mapping for this key.
125 * @param {String} key key to look for
127 contains: function (key) {
128 if (key === undefined) { return false; }
129 if (key in this.map) {
133 return this.parent.contains(key);
138 * Tries a number of keys, and returns the first object matching one of
141 * @param {Array} keys a sequence of keys to fetch the object for
142 * @returns {Class} the first class found matching an object
144 get_any: function (keys) {
145 for (var i=0; i<keys.length; ++i) {
147 if (!this.contains(key)) {
151 return this.get_object(key);
156 * Adds a new key and value to the registry.
158 * This method can be chained.
160 * @param {String} key
161 * @param {String} object_path fully qualified dotted object path
162 * @returns {instance.web.Registry} itself
164 add: function (key, object_path) {
165 this.map[key] = object_path;
169 * Creates and returns a copy of the current mapping, with the provided
170 * mapping argument added in (replacing existing keys if needed)
172 * Parent and child remain linked, a new key in the parent (which is not
173 * overwritten by the child) will appear in the child.
175 * @param {Object} [mapping={}] a mapping of keys to object-paths
177 extend: function (mapping) {
178 var child = new instance.web.Registry(mapping);
183 * @deprecated use Registry#extend
185 clone: function (mapping) {
186 console.warn('Registry#clone is deprecated, use Registry#extend');
187 return this.extend(mapping);
191 instance.web.py_eval = function(expr, context) {
192 return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null}));
196 Some retro-compatibility.
198 instance.web.JsonRPC = instance.web.Session;
200 /** Session openerp specific RPC class */
201 instance.web.Session.include( /** @lends instance.web.Session# */{
203 this._super.apply(this, arguments);
204 this.debug = ($.deparam($.param.querystring()).debug !== undefined);
205 // TODO: session store in cookie should be optional
206 this.name = instance._session_id;
207 this.qweb_mutex = new $.Mutex();
212 session_bind: function(origin) {
215 instance.web.qweb.default_dict['_s'] = this.origin;
217 this.username = null;
218 this.user_context= {};
220 this.module_list = instance._modules.slice();
221 this.module_loaded = {};
222 _(this.module_list).each(function (mod) {
223 self.module_loaded[mod] = true;
225 this.active_id = null;
226 return this.session_init();
229 * Init a session, reloads from cookie, if it exists
231 session_init: function () {
233 return this.session_reload().then(function(result) {
234 var modules = instance._modules.join(',');
235 var deferred = self.load_qweb(modules);
236 if(self.session_is_valid()) {
237 return deferred.then(function() { return self.load_modules(); });
241 self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).then(function(trans) {
242 instance.web._t.database.set_bundle(trans);
247 session_is_valid: function() {
248 var db = $.deparam.querystring().db;
249 if (db && this.db !== db) {
255 * The session is validated by restoration of a previous session
257 session_authenticate: function() {
259 return $.when(this._super.apply(this, arguments)).then(function() {
260 return self.load_modules();
263 session_logout: function() {
265 return this.rpc("/web/session/destroy", {});
267 get_cookie: function (name) {
268 if (!this.name) { return null; }
269 var nameEQ = this.name + '|' + name + '=';
270 var cookies = document.cookie.split(';');
271 for(var i=0; i<cookies.length; ++i) {
272 var cookie = cookies[i].replace(/^\s*/, '');
273 if(cookie.indexOf(nameEQ) === 0) {
275 return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
277 // wrong cookie, delete it
278 this.set_cookie(name, '', -1);
285 * Create a new cookie with the provided name and value
288 * @param name the cookie's name
289 * @param value the cookie's value
290 * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
292 set_cookie: function (name, value, ttl) {
293 if (!this.name) { return; }
294 ttl = ttl || 24*60*60*365;
296 this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
299 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
303 * Load additional web addons of that instance and init them
306 load_modules: function() {
308 return this.rpc('/web/session/modules', {}).then(function(result) {
309 var all_modules = _.uniq(self.module_list.concat(result));
310 var to_load = _.difference(result, self.module_list).join(',');
311 self.module_list = all_modules;
313 var loaded = self.load_translations();
314 var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
315 var file_list = [ locale ];
319 self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)),
320 self.load_qweb(to_load),
321 self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) {
322 file_list = file_list.concat(files);
326 return loaded.then(function () {
327 return self.load_js(file_list);
329 self.on_modules_loaded();
330 self.trigger('module_loaded');
331 if (!Date.CultureInfo.pmDesignator) {
332 // If no am/pm designator is specified but the openerp
333 // datetime format uses %i, date.js won't be able to
334 // correctly format a date. See bug#938497.
335 Date.CultureInfo.amDesignator = 'AM';
336 Date.CultureInfo.pmDesignator = 'PM';
341 load_translations: function() {
342 return instance.web._t.database.load_translations(this, this.module_list, this.user_context.lang);
344 load_css: function (files) {
346 _.each(files, function (file) {
347 openerp.loadCSS(self.url(file, null));
350 load_js: function(files) {
352 var d = $.Deferred();
353 if (files.length !== 0) {
354 var file = files.shift();
355 var url = self.url(file, null);
356 openerp.loadJS(url).done(d.resolve);
362 load_qweb: function(mods) {
364 self.qweb_mutex.exec(function() {
365 return self.rpc('/web/proxy/load', {path: '/web/webclient/qweb?mods=' + mods}).then(function(xml) {
366 if (!xml) { return; }
367 instance.web.qweb.add_template(_.str.trim(xml));
370 return self.qweb_mutex.def;
372 on_modules_loaded: function() {
373 for(var j=0; j<this.module_list.length; j++) {
374 var mod = this.module_list[j];
375 if(this.module_loaded[mod])
379 var fct = instance._openerp[mod];
380 if(typeof(fct) === "function") {
381 instance._openerp[mod] = {};
383 instance._openerp[mod][k] = fct[k];
385 fct(instance, instance._openerp[mod]);
387 this.module_loaded[mod] = true;
391 * Cooperative file download implementation, for ajaxy APIs.
393 * Requires that the server side implements an httprequest correctly
394 * setting the `fileToken` cookie to the value provided as the `token`
395 * parameter. The cookie *must* be set on the `/` path and *must not* be
398 * It would probably also be a good idea for the response to use a
399 * `Content-Disposition: attachment` header, especially if the MIME is a
400 * "known" type (e.g. text/plain, or for some browsers application/json
402 * @param {Object} options
403 * @param {String} [options.url] used to dynamically create a form
404 * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data
405 * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
406 * @param {Function} [options.success] callback in case of download success
407 * @param {Function} [options.error] callback in case of request error, provided with the error body
408 * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
410 get_file: function (options) {
411 // need to detect when the file is done downloading (not used
412 // yet, but we'll need it to fix the UI e.g. with a throbber
413 // while dump is being generated), iframe load event only fires
414 // when the iframe content loads, so we need to go smarter:
415 // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
416 var timer, token = new Date().getTime(),
417 cookie_name = 'fileToken', cookie_length = cookie_name.length,
418 CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
422 // iOS devices doesn't allow iframe use the way we do it,
423 // opening a new window seems the best way to workaround
424 if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
425 var params = _.extend({}, options.data || {}, {token: token});
426 var url = this.url(options.url, params);
427 instance.web.unblockUI();
428 return window.open(url);
431 var $form, $form_data = $('<div>');
433 var complete = function () {
434 if (options.complete) { options.complete(); }
438 if (remove_form && $form) { $form.remove(); }
440 var $target = $('<iframe style="display: none;">')
441 .attr({id: id, name: id})
442 .appendTo(document.body)
446 var body = this.contentDocument.body;
447 var node = body.childNodes[1] || body.childNodes[0];
448 options.error(JSON.parse(node.textContent));
456 $form = $(options.form);
459 $form = $('<form>', {
462 }).appendTo(document.body);
465 var hparams = _.extend({}, options.data || {}, {token: token});
466 if (this.override_session)
467 hparams.session_id = this.session_id;
468 _.each(hparams, function (value, key) {
469 var $input = $form.find('[name=' + key +']');
470 if (!$input.length) {
471 $input = $('<input type="hidden" name="' + key + '">')
472 .appendTo($form_data);
482 var waitLoop = function () {
483 var cookies = document.cookie.split(';');
485 timer = setTimeout(waitLoop, CHECK_INTERVAL);
486 for (var i=0; i<cookies.length; ++i) {
487 var cookie = cookies[i].replace(/^\s*/, '');
488 if (!cookie.indexOf(cookie_name === 0)) { continue; }
489 var cookie_val = cookie.substring(cookie_length + 1);
490 if (parseInt(cookie_val, 10) !== token) { continue; }
493 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
494 cookie_name, new Date().toGMTString());
495 if (options.success) { options.success(); }
500 timer = setTimeout(waitLoop, CHECK_INTERVAL);
502 synchronized_mode: function(to_execute) {
503 var synch = this.synch;
515 * Event Bus used to bind events scoped in the current instance
517 instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, {
519 instance.web.EventDispatcherMixin.init.call(this, parent);
521 // TODO fme: allow user to bind keys for some global actions.
522 // check gtk bindings
523 // http://unixpapa.com/js/key.html
524 _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
525 $('html').on(evtype, function(ev) {
526 self.trigger(evtype, ev);
529 _.each('resize,scroll'.split(','), function(evtype) {
530 $(window).on(evtype, function(ev) {
531 self.trigger(evtype, ev);
536 instance.web.bus = new instance.web.Bus();
538 instance.web.TranslationDataBase.include({
539 set_bundle: function(translation_bundle) {
540 this._super(translation_bundle);
541 if (translation_bundle.lang_parameters) {
542 this.parameters.grouping = py.eval(this.parameters.grouping);
547 /** Custom jQuery plugins */
548 $.browser = $.browser || {};
549 if(navigator.appVersion.indexOf("MSIE") !== -1) {
552 $.fn.getAttributes = function() {
555 for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
556 attr = attrs.item(i);
557 o[attr.nodeName] = attr.value;
562 $.fn.openerpClass = function(additionalClass) {
563 // This plugin should be applied on top level elements
564 additionalClass = additionalClass || '';
565 if (!!$.browser.msie) {
566 additionalClass += ' openerp_ie';
568 return this.each(function() {
569 $(this).addClass('openerp ' + additionalClass);
572 $.fn.openerpBounce = function() {
573 return this.each(function() {
574 $(this).css('box-sizing', 'content-box').effect('bounce', {distance: 18, times: 5}, 250);
578 /** Jquery extentions */
579 $.Mutex = openerp.Mutex;
581 $.async_when = function() {
583 var def = $.Deferred();
584 $.when.apply($, arguments).done(function() {
585 var args = arguments;
586 var action = function() {
587 def.resolve.apply(def, args);
592 setTimeout(action, 0);
594 var args = arguments;
595 var action = function() {
596 def.reject.apply(def, args);
601 setTimeout(action, 0);
607 // special tweak for the web client
608 var old_async_when = $.async_when;
609 $.async_when = function() {
610 if (instance.session.synch)
611 return $.when.apply(this, arguments);
613 return old_async_when.apply(this, arguments);
616 /** Setup default session */
617 instance.session = new instance.web.Session();
620 * Lazy translation function, only performs the translation when actually
621 * printed (e.g. inserted into a template)
623 * Useful when defining translatable strings in code evaluated before the
624 * translation database is loaded, as class attributes or at the top-level of
625 * an OpenERP Web module
627 * @param {String} s string to translate
628 * @returns {Object} lazy translation object
630 instance.web._lt = function (s) {
631 return {toString: function () { return instance.web._t(s); }};
633 instance.web.qweb.debug = instance.session.debug;
634 _.extend(instance.web.qweb.default_dict, {
635 '__debug__': instance.session.debug,
637 instance.web.qweb.preprocess_node = function() {
638 // Note that 'this' is the Qweb Node
639 switch (this.node.nodeType) {
641 case Node.CDATA_SECTION_NODE:
643 var translation = this.node.parentNode.attributes['t-translation'];
644 if (translation && translation.value === 'off') {
647 var match = /^(\s*)([\s\S]+?)(\s*)$/.exec(this.node.data);
649 this.node.data = match[1] + instance.web._t(match[2]) + match[3];
652 case Node.ELEMENT_NODE:
654 var attr, attrs = ['label', 'title', 'alt', 'placeholder'];
655 while ((attr = attrs.pop())) {
656 if (this.attributes[attr]) {
657 this.attributes[attr] = instance.web._t(this.attributes[attr]);
663 /** Setup jQuery timeago */
664 var _t = instance.web._t;
666 * Strings in timeago are "composed" with prefixes, words and suffixes. This
667 * makes their detection by our translating system impossible. Use all literal
668 * strings we're using with a translation mark here so the extractor can do its
672 _t('less than a minute ago');
673 _t('about a minute ago');
674 _t('%d minutes ago');
675 _t('about an hour ago');
679 _t('about a month ago');
681 _t('about a year ago');
685 instance.session.on('module_loaded', this, function () {
686 // provide timeago.js with our own translator method
687 $.timeago.settings.translator = instance.web._t;
692 $.blockUI.defaults.baseZ = 1100;
693 $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">';
694 $.blockUI.defaults.css.border = '0';
695 $.blockUI.defaults.css["background-color"] = '';
698 var messages_by_seconds = function() {
700 [0, _t("Loading...")],
701 [20, _t("Still loading...")],
702 [60, _t("Still loading...<br />Please be patient.")],
703 [120, _t("Don't leave yet,<br />it's still loading...")],
704 [300, _t("You may not believe it,<br />but the application is actually loading...")],
705 [420, _t("Take a minute to get a coffee,<br />because it's loading...")],
706 [3600, _t("Maybe you should consider reloading the application by pressing F5...")]
710 instance.web.Throbber = instance.web.Widget.extend({
711 template: "Throbber",
714 lines: 13, // The number of lines to draw
715 length: 7, // The length of each line
716 width: 4, // The line thickness
717 radius: 10, // The radius of the inner circle
718 rotate: 0, // The rotation offset
719 color: '#FFF', // #rgb or #rrggbb
720 speed: 1, // Rounds per second
721 trail: 60, // Afterglow percentage
722 shadow: false, // Whether to render a shadow
723 hwaccel: false, // Whether to use hardware acceleration
724 className: 'spinner', // The CSS class to assign to the spinner
725 zIndex: 2e9, // The z-index (defaults to 2000000000)
726 top: 'auto', // Top position relative to parent in px
727 left: 'auto' // Left position relative to parent in px
729 this.spin = new Spinner(opts).spin(this.$el[0]);
730 this.start_time = new Date().getTime();
733 act_message: function() {
735 setTimeout(function() {
736 if (self.isDestroyed())
738 var seconds = (new Date().getTime() - self.start_time) / 1000;
740 _.each(messages_by_seconds(), function(el) {
741 if (seconds >= el[0])
744 self.$(".oe_throbber_message").html(mes);
748 destroy: function() {
754 instance.web.Throbber.throbbers = [];
756 instance.web.blockUI = function() {
757 var tmp = $.blockUI.apply($, arguments);
758 var throbber = new instance.web.Throbber();
759 instance.web.Throbber.throbbers.push(throbber);
760 throbber.appendTo($(".oe_blockui_spin_container"));
763 instance.web.unblockUI = function() {
764 _.each(instance.web.Throbber.throbbers, function(el) {
767 return $.unblockUI.apply($, arguments);
771 /* Bootstrap defaults overwrite */
772 $.fn.tooltip.Constructor.DEFAULTS.placement = 'auto top';
773 $.fn.tooltip.Constructor.DEFAULTS.html = true;
774 $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover focus click';
775 $.fn.tooltip.Constructor.DEFAULTS.container = 'body';
776 //overwrite bootstrap tooltip method to prevent showing 2 tooltip at the same time
777 var bootstrap_show_function = $.fn.tooltip.Constructor.prototype.show;
778 $.fn.modal.Constructor.prototype.enforceFocus = function () { };
779 $.fn.tooltip.Constructor.prototype.show = function () {
780 $('.tooltip').remove();
781 //the following fix the bug when using placement
782 //auto and the parent element does not exist anymore resulting in
783 //an error. This should be remove once we updade bootstrap to a version that fix the bug
784 //edit: bug has been fixed here : https://github.com/twbs/bootstrap/pull/13752
785 var e = $.Event('show.bs.' + this.type);
786 var inDom = $.contains(document.documentElement, this.$element[0]);
787 if (e.isDefaultPrevented() || !inDom) return;
788 return bootstrap_show_function.call(this);
792 * Registry for all the client actions key: tag value: widget
794 instance.web.client_actions = new instance.web.Registry();
798 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: