diff options
Diffstat (limited to 'addons/web/static/src/js/corelib.js')
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 354 |
1 files changed, 207 insertions, 147 deletions
diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 93796ab4282..6ecbfdd01ac 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -99,7 +99,6 @@ openerp.web.corelib = function(instance) { for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { return function() { @@ -122,6 +121,10 @@ openerp.web.corelib = function(instance) { // The dummy class constructor function Class() { + if(this.constructor !== instance.web.Class){ + throw new Error("You can only instanciate objects with the 'new' operator"); + return null; + } // All construction is actually done in the init method if (!initializing && this.init) { var ret = this.init.apply(this, arguments); @@ -288,6 +291,17 @@ var Events = instance.web.Class.extend({ return this; }, + callbackList: function() { + var lst = []; + _.each(this._callbacks || {}, function(el, eventName) { + var node = el; + while ((node = node.next) && node.next) { + lst.push([eventName, node.callback, node.context]); + } + }); + return lst; + }, + trigger : function(events) { var event, node, calls, tail, args, all, rest; if (!(calls = this._callbacks)) @@ -331,6 +345,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { }, on: function(events, dest, func) { var self = this; + if (!(func instanceof Function)) { + throw new Error("Event handler must be a function."); + } events = events.split(/\s+/); _.each(events, function(eventName) { self.__edispatcherEvents.on(eventName, func, dest); @@ -363,9 +380,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { event.source.__edispatcherEvents.off(event.name, event.func, self); }); this.__edispatcherRegisteredEvents = []; - if(!this.__edispatcherEvents) { - debugger; - } + _.each(this.__edispatcherEvents.callbackList(), function(cal) { + this.off(cal[0], cal[2], cal[1]); + }, this); this.__edispatcherEvents.off(); instance.web.ParentedMixin.destroy.call(this); } @@ -376,7 +393,17 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(arg1, arg2, arg3) { + var map; + var options; + if (typeof arg1 === "string") { + map = {}; + map[arg1] = arg2; + options = arg3 || {}; + } else { + map = arg1; + options = arg2 || {}; + } var self = this; var changed = false; _.each(map, function(val, key) { @@ -385,10 +412,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { return; changed = true; self.__getterSetterInternalMap[key] = val; - self.trigger("change:" + key, self, { - oldValue: tmp, - newValue: val - }); + if (! options.silent) + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); }); if (changed) self.trigger("change", self); @@ -402,71 +430,16 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { init: function() { instance.web.PropertiesMixin.init.call(this); var self = this; - var callback_maker = function(obj, name, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - self.trigger.apply(self, [name].concat(args)); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - var result = c.callback.apply(c.self, c.args.concat(args)); - if (c.callback === method) { - // return the result of the original method - r = result; - } - // TODO special value to stop the chain - // instance.web.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; - } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); - } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" - }); - }; - callback.remove = function(f) { - callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { - return el.callback === f; - })); - return callback; - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 3) - }); - }; // Transform on_/do_* methods into callbacks + var callback_maker = function(fn) { + return function() { + return fn.apply(self, arguments); + } + }; for (var name in this) { if(typeof(this[name]) == "function") { - this[name].debug_name = name; if((/^on_|^do_/).test(name)) { - this[name] = callback_maker(this, name, this[name]); + this[name] = callback_maker(this[name]); } } } @@ -487,24 +460,20 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { * * The semantics of this precisely replace closing over the method call. * - * @param {String} method_name name of the method to invoke + * @param {String|Function} method function or name of the method to invoke * @returns {Function} proxied method */ - proxy: function (method_name) { + proxy: function (method) { var self = this; return function () { - return self[method_name].apply(self, arguments); + var fn = (typeof method === 'string') ? self[method] : method; + return fn.apply(self, arguments); } } }); instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { /** - * Tag name when creating a default $element. - * @type string - */ - tagName: 'div', - /** * Constructs the widget and sets its parent if a parent is given. * * @constructs instance.web.Widget @@ -513,14 +482,9 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.CallbackEnabledMixin.init.call(this); - this.$element = $(document.createElement(this.tagName)); this.setParent(parent); }, /** @@ -530,8 +494,8 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { _.each(this.getChildren(), function(el) { el.destroy(); }); - if(this.$element != null) { - this.$element.remove(); + if(this.$el) { + this.$el.remove(); } instance.web.PropertiesMixin.destroy.call(this); }, @@ -543,7 +507,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { appendTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.appendTo(t); + self.$el.appendTo(t); }, target); }, /** @@ -554,7 +518,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { prependTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.prependTo(t); + self.$el.prependTo(t); }, target); }, /** @@ -565,7 +529,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertAfter: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertAfter(t); + self.$el.insertAfter(t); }, target); }, /** @@ -576,7 +540,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertBefore: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertBefore(t); + self.$el.insertBefore(t); }, target); }, /** @@ -586,7 +550,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { */ replace: function(target) { return this.__widgetRenderAndInsert(_.bind(function(t) { - this.$element.replaceAll(t); + this.$el.replaceAll(t); }, this), target); }, __widgetRenderAndInsert: function(insertion, target) { @@ -609,6 +573,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @returns {jQuery.Deferred} */ start: function() { + return $.when(); } }); @@ -644,8 +609,8 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * // stuff that you want to init before the rendering * }, * start: function() { - * // stuff you want to make after the rendering, `this.$element` holds a correct value - * this.$element.find(".my_button").click(/* an example of event binding * /); + * // stuff you want to make after the rendering, `this.$el` holds a correct value + * this.$el.find(".my_button").click(/* an example of event binding * /); * * // if you have some asynchronous operations, it's a good idea to return * // a promise in start() @@ -669,6 +634,12 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * That will kill the widget in a clean way and erase its content from the dom. */ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { + // Backbone-ish API + tagName: 'div', + id: null, + className: null, + attributes: {}, + events: {}, /** * The name of the QWeb template that will be used for rendering. Must be * redefined in subclasses or the default render() method can not be used. @@ -685,14 +656,12 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.WidgetMixin.init.call(this,parent); - this.session = instance.connection; + // FIXME: this should not be + this.setElement(this._make_descriptive()); + this.session = instance.session; }, /** * Renders the element. The default implementation renders the widget using QWeb, @@ -700,29 +669,130 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * key that references `this`. */ renderElement: function() { - var rendered = null; - if (this.template) - rendered = instance.web.qweb.render(this.template, {widget: this}); - if (_.str.trim(rendered)) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; + var $el; + if (this.template) { + $el = $(_.str.trim(instance.web.qweb.render( + this.template, {widget: this}))); + } else { + $el = this._make_descriptive(); + } + this.replaceElement($el); + }, + + /** + * Re-sets the widget's root element and replaces the old root element + * (if any) by the new one in the DOM. + * + * @param {HTMLElement | jQuery} $el + * @returns {*} this + */ + replaceElement: function ($el) { + var $oldel = this.$el; + this.setElement($el); + if ($oldel && !$oldel.is(this.$el)) { + $oldel.replaceWith(this.$el); } + return this; }, /** - * Shortcut for $element.find() like backbone + * Re-sets the widget's root element (el/$el/$el). + * + * Includes: + * * re-delegating events + * * re-binding sub-elements + * * if the widget already had a root element, replacing the pre-existing + * element in the DOM + * + * @param {HTMLElement | jQuery} element new root element for the widget + * @return {*} this */ - "$": function() { - return this.$element.find.apply(this.$element,arguments); + setElement: function (element) { + // NB: completely useless, as WidgetMixin#init creates a $el + // always + if (this.$el) { + this.undelegateEvents(); + } + + this.$el = (element instanceof $) ? element : $(element); + this.el = this.$el[0]; + + this.delegateEvents(); + + return this; + }, + /** + * Utility function to build small DOM elements. + * + * @param {String} tagName name of the DOM element to create + * @param {Object} [attributes] map of DOM attributes to set on the element + * @param {String} [content] HTML content to set on the element + * @return {Element} + */ + make: function (tagName, attributes, content) { + var el = document.createElement(tagName); + if (!_.isEmpty(attributes)) { + $(el).attr(attributes); + } + if (content) { + $(el).html(content); + } + return el; + }, + /** + * Makes a potential root element from the declarative builder of the + * widget + * + * @return {jQuery} + * @private + */ + _make_descriptive: function () { + var attrs = _.extend({}, this.attributes || {}); + if (this.id) { attrs.id = this.id; } + if (this.className) { attrs['class'] = this.className; } + return $(this.make(this.tagName, attrs)); + }, + delegateEvents: function () { + var events = this.events; + if (_.isEmpty(events)) { return; } + + for(var key in events) { + if (!events.hasOwnProperty(key)) { continue; } + + var method = this.proxy(events[key]); + + var match = /^(\S+)(\s+(.*))?$/.exec(key); + var event = match[1]; + var selector = match[3]; + + event += '.widget_events'; + if (!selector) { + this.$el.on(event, method); + } else { + this.$el.on(event, selector, method); + } + } + }, + undelegateEvents: function () { + this.$el.off('.widget_events'); + }, + /** + * Shortcut for ``this.$el.find(selector)`` + * + * @param {String} selector CSS selector, rooted in $el + * @returns {jQuery} selector match + */ + $: function(selector) { + return this.$el.find(selector); }, /** * 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(action, on_finished) { - if (this.getParent()) { - return this.getParent().do_action(action, on_finished); + do_action: function() { + var parent = this.getParent(); + if (parent) { + return parent.do_action.apply(parent, arguments); } return false; }, @@ -741,7 +811,7 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { rpc: function(url, data, success, error) { var def = $.Deferred().then(success, error); var self = this; - instance.connection.rpc(url, data). then(function() { + instance.session.rpc(url, data).then(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }, function() { @@ -763,7 +833,7 @@ instance.web.Registry = instance.web.Class.extend({ * * 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 - * connection object). + * session object). * * @constructs instance.web.Registry * @param {Object} mapping a mapping of keys to object-paths @@ -872,6 +942,12 @@ instance.web.Registry = instance.web.Class.extend({ }); instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ + triggers: { + 'request': 'Request sent', + 'response': 'Response received', + 'response_failed': 'HTTP Error response or timeout received', + 'error': 'The received response is an JSON-RPC error', + }, /** * @constructs instance.web.JsonRPC * @extends instance.web.CallbackEnabled @@ -1035,7 +1111,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ uid: new py.float(this.uid), datetime: datetime, time: time, - relativedelta: relativedelta + relativedelta: relativedelta, + current_date: date.today.__call__().strftime(['%Y-%m-%d']) }; }, /** @@ -1193,7 +1270,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params, success_callback, error_callback) { + rpc: function(url, params) { var self = this; // url can be an $.ajax option object if (_.isString(url)) { @@ -1209,12 +1286,10 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.on_rpc_request(); - var aborter = params.aborter; - delete params.aborter; + this.trigger('request', url, payload); var request = this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { - self.on_rpc_response(); + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1222,18 +1297,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ deferred.resolve(response["result"], textStatus, jqXHR); } else if (response.error.data.type === "session_invalid") { self.uid = false; - // TODO deprecate or use a deferred on login.do_ask_login() - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() { deferred.resolve.apply(deferred, arguments); }, - function() { deferred.reject.apply(deferred, arguments); }); - }); } else { deferred.reject(response.error, $.Event()); } }, function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); + self.trigger('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1241,24 +1310,14 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }; deferred.reject(error, $.Event()); }); - if (aborter) { - aborter.abort_last = function () { - if (!(request.isResolved() || request.isRejected())) { - deferred.fail(function (error, event) { - event.preventDefault(); - }); - request.abort(); - } - }; - } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { - self.on_rpc_error(error, event); + self.trigger('error', error, event); } }); - }).then(success_callback, error_callback).promise(); + }); return deferred; }, /** @@ -1276,7 +1335,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ processData: false }, url); if (this.synch) - ajax.async = false; + ajax.async = false; return $.ajax(ajax); }, rpc_jsonp: function(url, payload) { @@ -1295,7 +1354,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ data: data }, url); if (this.synch) - ajax.async = false; + ajax.async = false; var payload_str = JSON.stringify(payload); var payload_url = $.param({r:payload_str}); if(payload_url.length < 2000) { @@ -1305,19 +1364,19 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); - var display = options.openerp.debug ? 'block' : 'none'; + var display = self.debug ? 'block' : 'none'; var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display)); var $form = $('<form>') .attr('method', 'POST') .attr('target', ifid) .attr('enctype', "multipart/form-data") - .attr('action', ajax.url + '?' + $.param(data)) + .attr('action', ajax.url + '?jsonp=1&' + $.param(data)) .append($('<input type="hidden" name="r" />').attr('value', payload_str)) .hide() .appendTo($('body')); var cleanUp = function() { if ($iframe) { - $iframe.unbind("load").attr("src", "javascript:false;").remove(); + $iframe.unbind("load").remove(); } $form.remove(); }; @@ -1341,14 +1400,15 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ return deferred; } }, - on_rpc_request: function() { - }, - on_rpc_response: function() { - }, - on_rpc_error: function(error) { + get_url: function (file) { + return this.prefix + file; }, }); +instance.web.py_eval = function(expr, context) { + return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); +}; + } // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: |
