diff options
| author | niv-openerp <nicolas.vanhoren@openerp.com> | 2013-08-06 14:26:23 +0200 |
|---|---|---|
| committer | niv-openerp <nicolas.vanhoren@openerp.com> | 2013-08-06 14:26:23 +0200 |
| commit | c63a86c59381c09bf9a33d8719a893d5ff467c95 (patch) | |
| tree | 5d33f30a2c1135a9facf21d211f9e0ebc5af608e /addons/web/static/src/js/core.js | |
| parent | 392e9d260bb692e4fa404848e70c6dc7ea61c36a (diff) | |
[IMP] Merged corelib.js and coresetup.js into core.js
bzr revid: nicolas.vanhoren@openerp.com-20130806122623-pq6bpkjk65jx0fnp
Diffstat (limited to 'addons/web/static/src/js/core.js')
| -rw-r--r-- | addons/web/static/src/js/core.js | 801 |
1 files changed, 801 insertions, 0 deletions
diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js new file mode 100644 index 00000000000..24a3bfdbc88 --- /dev/null +++ b/addons/web/static/src/js/core.js @@ -0,0 +1,801 @@ + +(function() { + if (typeof(console) === "undefined") { + // Even IE9 only exposes console object if debug window opened + window.console = {}; + ('log error debug info warn assert clear dir dirxml trace group' + + ' groupCollapsed groupEnd time timeEnd profile profileEnd count' + + ' exception').split(/\s+/).forEach(function(property) { + console[property] = _.identity; + }); + } +})(); + +openerp.web.core = function(instance) { + +var ControllerMixin = { + /** + * Informs the action manager to do an action. This supposes that + * the action manager can be found amongst the ancestors of the current widget. + * If that's not the case this method will simply return `false`. + */ + do_action: function() { + var parent = this.getParent(); + if (parent) { + return parent.do_action.apply(parent, arguments); + } + return false; + }, + do_notify: function() { + if (this.getParent()) { + return this.getParent().do_notify.apply(this,arguments); + } + return false; + }, + do_warn: function() { + if (this.getParent()) { + return this.getParent().do_warn.apply(this,arguments); + } + return false; + }, + rpc: function(url, data, options) { + return this.alive(openerp.session.rpc(url, data, options)); + } +}; + +/** + A class containing common utility methods useful when working with OpenERP as well as the PropertiesMixin. +*/ +openerp.web.Controller = openerp.web.Class.extend(openerp.web.PropertiesMixin, ControllerMixin, { + /** + * Constructs the object and sets its parent if a parent is given. + * + * @param {openerp.web.Controller} parent Binds the current instance to the given Controller instance. + * When that controller is destroyed by calling destroy(), the current instance will be + * destroyed too. Can be null. + */ + init: function(parent) { + openerp.web.PropertiesMixin.init.call(this); + this.setParent(parent); + this.session = openerp.session; + }, +}); + +openerp.web.Widget.include(_.extend({}, ControllerMixin, { + init: function() { + this._super.apply(this, arguments); + this.session = openerp.session; + }, +})); + +instance.web.Registry = instance.web.Class.extend({ + /** + * Stores a mapping of arbitrary key (strings) to object paths (as strings + * as well). + * + * Resolves those paths at query time in order to always fetch the correct + * object, even if those objects have been overloaded/replaced after the + * registry was created. + * + * An object path is simply a dotted name from the instance root to the + * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP + * session object). + * + * @constructs instance.web.Registry + * @param {Object} mapping a mapping of keys to object-paths + */ + init: function (mapping) { + this.parent = null; + this.map = mapping || {}; + }, + /** + * Retrieves the object matching the provided key string. + * + * @param {String} key the key to fetch the object for + * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception + * @returns {Class} the stored class, to initialize or null if not found + */ + get_object: function (key, silent_error) { + var path_string = this.map[key]; + if (path_string === undefined) { + if (this.parent) { + return this.parent.get_object(key, silent_error); + } + if (silent_error) { return void 'nooo'; } + return null; + } + + var object_match = instance; + var path = path_string.split('.'); + // ignore first section + for(var i=1; i<path.length; ++i) { + object_match = object_match[path[i]]; + + if (object_match === undefined) { + if (silent_error) { return void 'noooooo'; } + return null; + } + } + return object_match; + }, + /** + * Checks if the registry contains an object mapping for this key. + * + * @param {String} key key to look for + */ + contains: function (key) { + if (key === undefined) { return false; } + if (key in this.map) { + return true; + } + if (this.parent) { + return this.parent.contains(key); + } + return false; + }, + /** + * Tries a number of keys, and returns the first object matching one of + * the keys. + * + * @param {Array} keys a sequence of keys to fetch the object for + * @returns {Class} the first class found matching an object + */ + get_any: function (keys) { + for (var i=0; i<keys.length; ++i) { + var key = keys[i]; + if (!this.contains(key)) { + continue; + } + + return this.get_object(key); + } + return null; + }, + /** + * Adds a new key and value to the registry. + * + * This method can be chained. + * + * @param {String} key + * @param {String} object_path fully qualified dotted object path + * @returns {instance.web.Registry} itself + */ + add: function (key, object_path) { + this.map[key] = object_path; + return this; + }, + /** + * Creates and returns a copy of the current mapping, with the provided + * mapping argument added in (replacing existing keys if needed) + * + * Parent and child remain linked, a new key in the parent (which is not + * overwritten by the child) will appear in the child. + * + * @param {Object} [mapping={}] a mapping of keys to object-paths + */ + extend: function (mapping) { + var child = new instance.web.Registry(mapping); + child.parent = this; + return child; + }, + /** + * @deprecated use Registry#extend + */ + clone: function (mapping) { + console.warn('Registry#clone is deprecated, use Registry#extend'); + return this.extend(mapping); + } +}); + +instance.web.py_eval = function(expr, context) { + return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); +}; + +/* + Some retro-compatibility. +*/ +instance.web.JsonRPC = instance.web.Session; + +/** Session openerp specific RPC class */ +instance.web.Session.include( /** @lends instance.web.Session# */{ + init: function() { + this._super.apply(this, arguments); + // TODO: session store in cookie should be optional + this.name = instance._session_id; + this.qweb_mutex = new $.Mutex(); + }, + /** + * Setup a sessionm + */ + session_bind: function(origin) { + var self = this; + this.setup(origin); + instance.web.qweb.default_dict['_s'] = this.origin; + this.uid = null; + this.username = null; + this.user_context= {}; + this.db = null; + this.module_list = instance._modules.slice(); + this.module_loaded = {}; + _(this.module_list).each(function (mod) { + self.module_loaded[mod] = true; + }); + this.active_id = null; + return this.session_init(); + }, + /** + * Init a session, reloads from cookie, if it exists + */ + session_init: function () { + var self = this; + return this.session_reload().then(function(result) { + var modules = instance._modules.join(','); + var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).then(self.load_qweb.bind(self)); + if(self.session_is_valid()) { + return deferred.then(function() { return self.load_modules(); }); + } + return $.when( + deferred, + self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).then(function(trans) { + instance.web._t.database.set_bundle(trans); + }) + ); + }); + }, + session_is_valid: function() { + var db = $.deparam.querystring().db; + if (db && this.db !== db) { + return false; + } + return !!this.uid; + }, + /** + * The session is validated either by login or by restoration of a previous session + */ + session_authenticate: function() { + var self = this; + return $.when(this._super.apply(this, arguments)).then(function() { + return self.load_modules(); + }); + }, + session_logout: function() { + $.bbq.removeState(); + return this.rpc("/web/session/destroy", {}); + }, + get_cookie: function (name) { + if (!this.name) { return null; } + var nameEQ = this.name + '|' + name + '='; + var cookies = document.cookie.split(';'); + for(var i=0; i<cookies.length; ++i) { + var cookie = cookies[i].replace(/^\s*/, ''); + if(cookie.indexOf(nameEQ) === 0) { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } + } + return null; + }, + /** + * Create a new cookie with the provided name and value + * + * @private + * @param name the cookie's name + * @param value the cookie's value + * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete + */ + set_cookie: function (name, value, ttl) { + if (!this.name) { return; } + ttl = ttl || 24*60*60*365; + document.cookie = [ + this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)), + 'path=/', + 'max-age=' + ttl, + 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString() + ].join(';'); + }, + /** + * Load additional web addons of that instance and init them + * + */ + load_modules: function() { + var self = this; + return this.rpc('/web/session/modules', {}).then(function(result) { + var all_modules = _.uniq(self.module_list.concat(result)); + var to_load = _.difference(result, self.module_list).join(','); + self.module_list = all_modules; + + var loaded = self.load_translations(); + var datejs_locale = "/web/static/lib/datejs/globalization/" + self.user_context.lang.replace("_", "-") + ".js"; + + var file_list = [ datejs_locale ]; + if(to_load.length) { + loaded = $.when( + loaded, + self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)), + self.rpc('/web/webclient/qweblist', {mods: to_load}).then(self.load_qweb.bind(self)), + self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) { + file_list = file_list.concat(files); + }) + ); + } + return loaded.then(function () { + return self.load_js(file_list); + }).done(function() { + self.on_modules_loaded(); + self.trigger('module_loaded'); + if (!Date.CultureInfo.pmDesignator) { + // If no am/pm designator is specified but the openerp + // datetime format uses %i, date.js won't be able to + // correctly format a date. See bug#938497. + Date.CultureInfo.amDesignator = 'AM'; + Date.CultureInfo.pmDesignator = 'PM'; + } + }); + }); + }, + load_translations: function() { + return instance.web._t.database.load_translations(this, this.module_list, this.user_context.lang); + }, + load_css: function (files) { + var self = this; + _.each(files, function (file) { + $('head').append($('<link>', { + 'href': self.url(file, null), + 'rel': 'stylesheet', + 'type': 'text/css' + })); + }); + }, + load_js: function(files) { + var self = this; + var d = $.Deferred(); + if(files.length !== 0) { + var file = files.shift(); + var tag = document.createElement('script'); + tag.type = 'text/javascript'; + tag.src = self.url(file, null); + tag.onload = tag.onreadystatechange = function() { + if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) + return; + tag.onload_done = true; + self.load_js(files).done(function () { + d.resolve(); + }); + }; + var head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(tag); + } else { + d.resolve(); + } + return d; + }, + load_qweb: function(files) { + var self = this; + _.each(files, function(file) { + self.qweb_mutex.exec(function() { + return self.rpc('/web/proxy/load', {path: file}).then(function(xml) { + if (!xml) { return; } + instance.web.qweb.add_template(_.str.trim(xml)); + }); + }); + }); + return self.qweb_mutex.def; + }, + on_modules_loaded: function() { + for(var j=0; j<this.module_list.length; j++) { + var mod = this.module_list[j]; + if(this.module_loaded[mod]) + continue; + instance[mod] = {}; + // init module mod + var fct = instance._openerp[mod]; + if(typeof(fct) === "function") { + instance._openerp[mod] = {}; + for (var k in fct) { + instance._openerp[mod][k] = fct[k]; + } + fct(instance, instance._openerp[mod]); + } + this.module_loaded[mod] = true; + } + }, + /** + * Cooperative file download implementation, for ajaxy APIs. + * + * Requires that the server side implements an httprequest correctly + * setting the `fileToken` cookie to the value provided as the `token` + * parameter. The cookie *must* be set on the `/` path and *must not* be + * `httpOnly`. + * + * It would probably also be a good idea for the response to use a + * `Content-Disposition: attachment` header, especially if the MIME is a + * "known" type (e.g. text/plain, or for some browsers application/json + * + * @param {Object} options + * @param {String} [options.url] used to dynamically create a form + * @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 + * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file + * @param {Function} [options.success] callback in case of download success + * @param {Function} [options.error] callback in case of request error, provided with the error body + * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed + */ + get_file: function (options) { + // need to detect when the file is done downloading (not used + // yet, but we'll need it to fix the UI e.g. with a throbber + // while dump is being generated), iframe load event only fires + // when the iframe content loads, so we need to go smarter: + // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx + var timer, token = new Date().getTime(), + cookie_name = 'fileToken', cookie_length = cookie_name.length, + CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'), + remove_form = false; + + + // iOS devices doesn't allow iframe use the way we do it, + // opening a new window seems the best way to workaround + if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) { + var params = _.extend({}, options.data || {}, {token: token}); + var url = this.url(options.url, params); + instance.web.unblockUI(); + return window.open(url); + } + + var $form, $form_data = $('<div>'); + + var complete = function () { + if (options.complete) { options.complete(); } + clearTimeout(timer); + $form_data.remove(); + $target.remove(); + if (remove_form && $form) { $form.remove(); } + }; + var $target = $('<iframe style="display: none;">') + .attr({id: id, name: id}) + .appendTo(document.body) + .load(function () { + try { + if (options.error) { + if (!this.contentDocument.body.childNodes[1]) { + options.error(this.contentDocument.body.childNodes); + } + else { + options.error(JSON.parse(this.contentDocument.body.childNodes[1].textContent)); + } + } + } finally { + complete(); + } + }); + + if (options.form) { + $form = $(options.form); + } else { + remove_form = true; + $form = $('<form>', { + action: options.url, + method: 'POST' + }).appendTo(document.body); + } + + var hparams = _.extend({}, options.data || {}, {token: token}); + if (this.override_session) + hparams.session_id = this.session_id; + _.each(hparams, function (value, key) { + var $input = $form.find('[name=' + key +']'); + if (!$input.length) { + $input = $('<input type="hidden" name="' + key + '">') + .appendTo($form_data); + } + $input.val(value); + }); + + $form + .append($form_data) + .attr('target', id) + .get(0).submit(); + + var waitLoop = function () { + var cookies = document.cookie.split(';'); + // setup next check + timer = setTimeout(waitLoop, CHECK_INTERVAL); + for (var i=0; i<cookies.length; ++i) { + var cookie = cookies[i].replace(/^\s*/, ''); + if (!cookie.indexOf(cookie_name === 0)) { continue; } + var cookie_val = cookie.substring(cookie_length + 1); + if (parseInt(cookie_val, 10) !== token) { continue; } + + // clear cookie + document.cookie = _.str.sprintf("%s=;expires=%s;path=/", + cookie_name, new Date().toGMTString()); + if (options.success) { options.success(); } + complete(); + return; + } + }; + timer = setTimeout(waitLoop, CHECK_INTERVAL); + }, + synchronized_mode: function(to_execute) { + var synch = this.synch; + this.synch = true; + try { + return to_execute(); + } finally { + this.synch = synch; + } + } +}); + + +/** + * Event Bus used to bind events scoped in the current instance + */ +instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, { + init: function() { + instance.web.EventDispatcherMixin.init.call(this, parent); + var self = this; + // TODO fme: allow user to bind keys for some global actions. + // check gtk bindings + // http://unixpapa.com/js/key.html + _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) { + $('html').on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + _.each('resize,scroll'.split(','), function(evtype) { + $(window).on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + } +}); +instance.web.bus = new instance.web.Bus(); + +instance.web.TranslationDataBase.include({ + set_bundle: function(translation_bundle) { + this._super(translation_bundle); + if (translation_bundle.lang_parameters) { + this.parameters.grouping = py.eval(this.parameters.grouping); + } + }, +}); + +/** Custom jQuery plugins */ +$.fn.getAttributes = function() { + var o = {}; + if (this.length) { + for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) { + attr = attrs.item(i); + o[attr.nodeName] = attr.nodeValue; + } + } + return o; +}; +$.fn.openerpClass = function(additionalClass) { + // This plugin should be applied on top level elements + additionalClass = additionalClass || ''; + if (!!$.browser.msie) { + additionalClass += ' openerp_ie'; + } + return this.each(function() { + $(this).addClass('openerp ' + additionalClass); + }); +}; +$.fn.openerpBounce = function() { + return this.each(function() { + $(this).css('box-sizing', 'content-box').effect('bounce', {distance: 18, times: 5}, 250); + }); +}; + +/** Jquery extentions */ +$.Mutex = (function() { + function Mutex() { + this.def = $.Deferred().resolve(); + } + Mutex.prototype.exec = function(action) { + var current = this.def; + var next = this.def = $.Deferred(); + return current.then(function() { + return $.when(action()).always(function() { + next.resolve(); + }); + }); + }; + return Mutex; +})(); + +$.async_when = function() { + var async = false; + var def = $.Deferred(); + $.when.apply($, arguments).done(function() { + var args = arguments; + var action = function() { + def.resolve.apply(def, args); + }; + if (async) + action(); + else + setTimeout(action, 0); + }).fail(function() { + var args = arguments; + var action = function() { + def.reject.apply(def, args); + }; + if (async) + action(); + else + setTimeout(action, 0); + }); + async = true; + return def; +}; + +// special tweak for the web client +var old_async_when = $.async_when; +$.async_when = function() { + if (instance.session.synch) + return $.when.apply(this, arguments); + else + return old_async_when.apply(this, arguments); +}; + +/** Setup default session */ +instance.session = new instance.web.Session(); + +/** + * Lazy translation function, only performs the translation when actually + * printed (e.g. inserted into a template) + * + * Useful when defining translatable strings in code evaluated before the + * translation database is loaded, as class attributes or at the top-level of + * an OpenERP Web module + * + * @param {String} s string to translate + * @returns {Object} lazy translation object + */ +instance.web._lt = function (s) { + return {toString: function () { return instance.web._t(s); }}; +}; +instance.web.qweb.debug = instance.session.debug; +_.extend(instance.web.qweb.default_dict, { + '__debug__': instance.session.debug, +}); +instance.web.qweb.preprocess_node = function() { + // Note that 'this' is the Qweb Node + switch (this.node.nodeType) { + case Node.TEXT_NODE: + case Node.CDATA_SECTION_NODE: + // Text and CDATAs + var translation = this.node.parentNode.attributes['t-translation']; + if (translation && translation.value === 'off') { + return; + } + var match = /^(\s*)([\s\S]+?)(\s*)$/.exec(this.node.data); + if (match) { + this.node.data = match[1] + instance.web._t(match[2]) + match[3]; + } + break; + case Node.ELEMENT_NODE: + // Element + var attr, attrs = ['label', 'title', 'alt', 'placeholder']; + while ((attr = attrs.pop())) { + if (this.attributes[attr]) { + this.attributes[attr] = instance.web._t(this.attributes[attr]); + } + } + } +}; + +/** Setup jQuery timeago */ +var _t = instance.web._t; +/* + * Strings in timeago are "composed" with prefixes, words and suffixes. This + * makes their detection by our translating system impossible. Use all literal + * strings we're using with a translation mark here so the extractor can do its + * job. + */ +{ + _t('less than a minute ago'); + _t('about a minute ago'); + _t('%d minutes ago'); + _t('about an hour ago'); + _t('%d hours ago'); + _t('a day ago'); + _t('%d days ago'); + _t('about a month ago'); + _t('%d months ago'); + _t('about a year ago'); + _t('%d years ago'); +} + +instance.session.on('module_loaded', this, function () { + // provide timeago.js with our own translator method + $.timeago.settings.translator = instance.web._t; +}); + +/** Setup blockui */ +if ($.blockUI) { + $.blockUI.defaults.baseZ = 1100; + $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; +} + +var messages_by_seconds = function() { + return [ + [0, _t("Loading...")], + [20, _t("Still loading...")], + [60, _t("Still loading...<br />Please be patient.")], + [120, _t("Don't leave yet,<br />it's still loading...")], + [300, _t("You may not believe it,<br />but the application is actually loading...")], + [420, _t("Take a minute to get a coffee,<br />because it's loading...")], + [3600, _t("Maybe you should consider reloading the application by pressing F5...")] + ]; +}; + +instance.web.Throbber = instance.web.Widget.extend({ + template: "Throbber", + start: function() { + var opts = { + lines: 13, // The number of lines to draw + length: 7, // The length of each line + width: 4, // The line thickness + radius: 10, // The radius of the inner circle + rotate: 0, // The rotation offset + color: '#FFF', // #rgb or #rrggbb + speed: 1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: false, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: 'auto', // Top position relative to parent in px + left: 'auto' // Left position relative to parent in px + }; + this.spin = new Spinner(opts).spin(this.$el[0]); + this.start_time = new Date().getTime(); + this.act_message(); + }, + act_message: function() { + var self = this; + setTimeout(function() { + if (self.isDestroyed()) + return; + var seconds = (new Date().getTime() - self.start_time) / 1000; + var mes; + _.each(messages_by_seconds(), function(el) { + if (seconds >= el[0]) + mes = el[1]; + }); + self.$(".oe_throbber_message").html(mes); + self.act_message(); + }, 1000); + }, + destroy: function() { + if (this.spin) + this.spin.stop(); + this._super(); + }, +}); +instance.web.Throbber.throbbers = []; + +instance.web.blockUI = function() { + var tmp = $.blockUI.apply($, arguments); + var throbber = new instance.web.Throbber(); + instance.web.Throbber.throbbers.push(throbber); + throbber.appendTo($(".oe_blockui_spin_container")); + return tmp; +}; +instance.web.unblockUI = function() { + _.each(instance.web.Throbber.throbbers, function(el) { + el.destroy(); + }); + return $.unblockUI.apply($, arguments); +}; + +/** + * Registry for all the client actions key: tag value: widget + */ +instance.web.client_actions = new instance.web.Registry(); + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: |
