summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/corelib.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/corelib.js')
-rw-r--r--addons/web/static/src/js/corelib.js354
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: