1131 lines
38 KiB
JavaScript
1131 lines
38 KiB
JavaScript
/*
|
|
* Copyright (c) 2012, OpenERP S.A.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
* list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
openerp.web.corelib = function(instance) {
|
|
|
|
/**
|
|
* Improved John Resig's inheritance, based on:
|
|
*
|
|
* Simple JavaScript Inheritance
|
|
* By John Resig http://ejohn.org/
|
|
* MIT Licensed.
|
|
*
|
|
* Adds "include()"
|
|
*
|
|
* Defines The Class object. That object can be used to define and inherit classes using
|
|
* the extend() method.
|
|
*
|
|
* Example:
|
|
*
|
|
* var Person = instance.web.Class.extend({
|
|
* init: function(isDancing){
|
|
* this.dancing = isDancing;
|
|
* },
|
|
* dance: function(){
|
|
* return this.dancing;
|
|
* }
|
|
* });
|
|
*
|
|
* The init() method act as a constructor. This class can be instancied this way:
|
|
*
|
|
* var person = new Person(true);
|
|
* person.dance();
|
|
*
|
|
* The Person class can also be extended again:
|
|
*
|
|
* var Ninja = Person.extend({
|
|
* init: function(){
|
|
* this._super( false );
|
|
* },
|
|
* dance: function(){
|
|
* // Call the inherited version of dance()
|
|
* return this._super();
|
|
* },
|
|
* swingSword: function(){
|
|
* return true;
|
|
* }
|
|
* });
|
|
*
|
|
* When extending a class, each re-defined method can use this._super() to call the previous
|
|
* implementation of that method.
|
|
*/
|
|
(function() {
|
|
var initializing = false,
|
|
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
|
|
// The web Class implementation (does nothing)
|
|
instance.web.Class = function(){};
|
|
|
|
/**
|
|
* Subclass an existing class
|
|
*
|
|
* @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
|
|
*/
|
|
instance.web.Class.extend = function() {
|
|
var _super = this.prototype;
|
|
// Support mixins arguments
|
|
var args = _.toArray(arguments);
|
|
args.unshift({});
|
|
var prop = _.extend.apply(_,args);
|
|
|
|
// Instantiate a web class (but only create the instance,
|
|
// don't run the init constructor)
|
|
initializing = true;
|
|
var prototype = new this();
|
|
initializing = false;
|
|
|
|
// Copy the properties over onto the new prototype
|
|
for (var name in prop) {
|
|
// Check if we're overwriting an existing function
|
|
prototype[name] = typeof prop[name] == "function" &&
|
|
fnTest.test(prop[name]) ?
|
|
(function(name, fn) {
|
|
return function() {
|
|
var tmp = this._super;
|
|
|
|
// Add a new ._super() method that is the same
|
|
// method but on the super-class
|
|
this._super = _super[name];
|
|
|
|
// The method only need to be bound temporarily, so
|
|
// we remove it when we're done executing
|
|
var ret = fn.apply(this, arguments);
|
|
this._super = tmp;
|
|
|
|
return ret;
|
|
};
|
|
})(name, prop[name]) :
|
|
prop[name];
|
|
}
|
|
|
|
// 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);
|
|
if (ret) { return ret; }
|
|
}
|
|
return this;
|
|
}
|
|
Class.include = function (properties) {
|
|
for (var name in properties) {
|
|
if (typeof properties[name] !== 'function'
|
|
|| !fnTest.test(properties[name])) {
|
|
prototype[name] = properties[name];
|
|
} else if (typeof prototype[name] === 'function'
|
|
&& prototype.hasOwnProperty(name)) {
|
|
prototype[name] = (function (name, fn, previous) {
|
|
return function () {
|
|
var tmp = this._super;
|
|
this._super = previous;
|
|
var ret = fn.apply(this, arguments);
|
|
this._super = tmp;
|
|
return ret;
|
|
}
|
|
})(name, properties[name], prototype[name]);
|
|
} else if (typeof _super[name] === 'function') {
|
|
prototype[name] = (function (name, fn) {
|
|
return function () {
|
|
var tmp = this._super;
|
|
this._super = _super[name];
|
|
var ret = fn.apply(this, arguments);
|
|
this._super = tmp;
|
|
return ret;
|
|
}
|
|
})(name, properties[name]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Populate our constructed prototype object
|
|
Class.prototype = prototype;
|
|
|
|
// Enforce the constructor to be what we expect
|
|
Class.constructor = Class;
|
|
|
|
// And make this class extendable
|
|
Class.extend = arguments.callee;
|
|
|
|
return Class;
|
|
};
|
|
})();
|
|
|
|
// Mixins
|
|
|
|
/**
|
|
* Mixin to structure objects' life-cycles folowing a parent-children
|
|
* relationship. Each object can a have a parent and multiple children.
|
|
* When an object is destroyed, all its children are destroyed too releasing
|
|
* any resource they could have reserved before.
|
|
*/
|
|
instance.web.ParentedMixin = {
|
|
__parentedMixin : true,
|
|
init: function() {
|
|
this.__parentedDestroyed = false;
|
|
this.__parentedChildren = [];
|
|
this.__parentedParent = null;
|
|
},
|
|
/**
|
|
* Set the parent of the current object. When calling this method, the
|
|
* parent will also be informed and will return the current object
|
|
* when its getChildren() method is called. If the current object did
|
|
* already have a parent, it is unregistered before, which means the
|
|
* previous parent will not return the current object anymore when its
|
|
* getChildren() method is called.
|
|
*/
|
|
setParent : function(parent) {
|
|
if (this.getParent()) {
|
|
if (this.getParent().__parentedMixin) {
|
|
this.getParent().__parentedChildren = _.without(this
|
|
.getParent().getChildren(), this);
|
|
}
|
|
}
|
|
this.__parentedParent = parent;
|
|
if (parent && parent.__parentedMixin) {
|
|
parent.__parentedChildren.push(this);
|
|
}
|
|
},
|
|
/**
|
|
* Return the current parent of the object (or null).
|
|
*/
|
|
getParent : function() {
|
|
return this.__parentedParent;
|
|
},
|
|
/**
|
|
* Return a list of the children of the current object.
|
|
*/
|
|
getChildren : function() {
|
|
return _.clone(this.__parentedChildren);
|
|
},
|
|
/**
|
|
* Returns true if destroy() was called on the current object.
|
|
*/
|
|
isDestroyed : function() {
|
|
return this.__parentedDestroyed;
|
|
},
|
|
/**
|
|
* Inform the object it should destroy itself, releasing any
|
|
* resource it could have reserved.
|
|
*/
|
|
destroy : function() {
|
|
_.each(this.getChildren(), function(el) {
|
|
el.destroy();
|
|
});
|
|
this.setParent(undefined);
|
|
this.__parentedDestroyed = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
|
|
*
|
|
* This class just handle the dispatching of events, it is not meant to be extended,
|
|
* nor used directly. All integration with parenting and automatic unregistration of
|
|
* events is done in EventDispatcherMixin.
|
|
*
|
|
* Copyright notice for the following Class:
|
|
*
|
|
* (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
|
* Backbone may be freely distributed under the MIT license.
|
|
* For all details and documentation:
|
|
* http://backbonejs.org
|
|
*
|
|
*/
|
|
var Events = instance.web.Class.extend({
|
|
on : function(events, callback, context) {
|
|
var ev;
|
|
events = events.split(/\s+/);
|
|
var calls = this._callbacks || (this._callbacks = {});
|
|
while (ev = events.shift()) {
|
|
var list = calls[ev] || (calls[ev] = {});
|
|
var tail = list.tail || (list.tail = list.next = {});
|
|
tail.callback = callback;
|
|
tail.context = context;
|
|
list.tail = tail.next = {};
|
|
}
|
|
return this;
|
|
},
|
|
|
|
off : function(events, callback, context) {
|
|
var ev, calls, node;
|
|
if (!events) {
|
|
delete this._callbacks;
|
|
} else if (calls = this._callbacks) {
|
|
events = events.split(/\s+/);
|
|
while (ev = events.shift()) {
|
|
node = calls[ev];
|
|
delete calls[ev];
|
|
if (!callback || !node)
|
|
continue;
|
|
while ((node = node.next) && node.next) {
|
|
if (node.callback === callback
|
|
&& (!context || node.context === context))
|
|
continue;
|
|
this.on(ev, node.callback, node.context);
|
|
}
|
|
}
|
|
}
|
|
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))
|
|
return this;
|
|
all = calls['all'];
|
|
(events = events.split(/\s+/)).push(null);
|
|
// Save references to the current heads & tails.
|
|
while (event = events.shift()) {
|
|
if (all)
|
|
events.push({
|
|
next : all.next,
|
|
tail : all.tail,
|
|
event : event
|
|
});
|
|
if (!(node = calls[event]))
|
|
continue;
|
|
events.push({
|
|
next : node.next,
|
|
tail : node.tail
|
|
});
|
|
}
|
|
rest = Array.prototype.slice.call(arguments, 1);
|
|
while (node = events.pop()) {
|
|
tail = node.tail;
|
|
args = node.event ? [ node.event ].concat(rest) : rest;
|
|
while ((node = node.next) !== tail) {
|
|
node.callback.apply(node.context || this, args);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
});
|
|
|
|
instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, {
|
|
__eventDispatcherMixin: true,
|
|
init: function() {
|
|
instance.web.ParentedMixin.init.call(this);
|
|
this.__edispatcherEvents = new Events();
|
|
this.__edispatcherRegisteredEvents = [];
|
|
},
|
|
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);
|
|
if (dest && dest.__eventDispatcherMixin) {
|
|
dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
|
|
}
|
|
});
|
|
return this;
|
|
},
|
|
off: function(events, dest, func) {
|
|
var self = this;
|
|
events = events.split(/\s+/);
|
|
_.each(events, function(eventName) {
|
|
self.__edispatcherEvents.off(eventName, func, dest);
|
|
if (dest && dest.__eventDispatcherMixin) {
|
|
dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
|
|
return !(el.name === eventName && el.func === func && el.source === self);
|
|
});
|
|
}
|
|
});
|
|
return this;
|
|
},
|
|
trigger: function(events) {
|
|
this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
|
|
return this;
|
|
},
|
|
destroy: function() {
|
|
var self = this;
|
|
_.each(this.__edispatcherRegisteredEvents, function(event) {
|
|
event.source.__edispatcherEvents.off(event.name, event.func, self);
|
|
});
|
|
this.__edispatcherRegisteredEvents = [];
|
|
_.each(this.__edispatcherEvents.callbackList(), function(cal) {
|
|
this.off(cal[0], cal[2], cal[1]);
|
|
}, this);
|
|
this.__edispatcherEvents.off();
|
|
instance.web.ParentedMixin.destroy.call(this);
|
|
}
|
|
});
|
|
|
|
instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, {
|
|
init: function() {
|
|
instance.web.EventDispatcherMixin.init.call(this);
|
|
this.__getterSetterInternalMap = {};
|
|
},
|
|
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) {
|
|
var tmp = self.__getterSetterInternalMap[key];
|
|
if (tmp === val)
|
|
return;
|
|
changed = true;
|
|
self.__getterSetterInternalMap[key] = val;
|
|
if (! options.silent)
|
|
self.trigger("change:" + key, self, {
|
|
oldValue: tmp,
|
|
newValue: val
|
|
});
|
|
});
|
|
if (changed)
|
|
self.trigger("change", self);
|
|
},
|
|
get: function(key) {
|
|
return this.__getterSetterInternalMap[key];
|
|
}
|
|
});
|
|
|
|
// Classes
|
|
|
|
/**
|
|
A class containing common utility methods useful when working with OpenERP as well as the PropertiesMixin.
|
|
*/
|
|
instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
|
/**
|
|
* Constructs the object and sets its parent if a parent is given.
|
|
*
|
|
* @param {instance.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) {
|
|
instance.web.PropertiesMixin.init.call(this);
|
|
this.setParent(parent);
|
|
},
|
|
/**
|
|
* Proxies a method of the object, in order to keep the right ``this`` on
|
|
* method invocations.
|
|
*
|
|
* This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
|
|
* even more so to ``jQuery.proxy`` with a fundamental difference: its
|
|
* resolution of the method being called is lazy, meaning it will use the
|
|
* method as it is when the proxy is called, not when the proxy is created.
|
|
*
|
|
* Other methods will fix the bound method to what it is when creating the
|
|
* binding/proxy, which is fine in most javascript code but problematic in
|
|
* OpenERP Web where developers may want to replace existing callbacks with
|
|
* theirs.
|
|
*
|
|
* The semantics of this precisely replace closing over the method call.
|
|
*
|
|
* @param {String|Function} method function or name of the method to invoke
|
|
* @returns {Function} proxied method
|
|
*/
|
|
proxy: function (method) {
|
|
var self = this;
|
|
return function () {
|
|
var fn = (typeof method === 'string') ? self[method] : method;
|
|
return fn.apply(self, arguments);
|
|
}
|
|
},
|
|
/**
|
|
* 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) {
|
|
var def = $.Deferred();
|
|
var self = this;
|
|
instance.session.rpc(url, data, options).done(function() {
|
|
if (!self.isDestroyed())
|
|
def.resolve.apply(def, arguments);
|
|
}).fail(function() {
|
|
if (!self.isDestroyed())
|
|
def.reject.apply(def, arguments);
|
|
});
|
|
return def.promise();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Base class for all visual components. Provides a lot of functionalities helpful
|
|
* for the management of a part of the DOM.
|
|
*
|
|
* Widget handles:
|
|
* - Rendering with QWeb.
|
|
* - Life-cycle management and parenting (when a parent is destroyed, all its children are
|
|
* destroyed too).
|
|
* - Insertion in DOM.
|
|
*
|
|
* Guide to create implementations of the Widget class:
|
|
* ==============================================
|
|
*
|
|
* Here is a sample child class:
|
|
*
|
|
* MyWidget = instance.base.Widget.extend({
|
|
* // the name of the QWeb template to use for rendering
|
|
* template: "MyQWebTemplate",
|
|
*
|
|
* init: function(parent) {
|
|
* this._super(parent);
|
|
* // stuff that you want to init before the rendering
|
|
* },
|
|
* start: function() {
|
|
* // 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()
|
|
* var promise = this.rpc(...);
|
|
* return promise;
|
|
* }
|
|
* });
|
|
*
|
|
* Now this class can simply be used with the following syntax:
|
|
*
|
|
* var my_widget = new MyWidget(this);
|
|
* my_widget.appendTo($(".some-div"));
|
|
*
|
|
* With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
|
|
* DOM inside the ".some-div" div and its events were binded.
|
|
*
|
|
* And of course, when you don't need that widget anymore, just do:
|
|
*
|
|
* my_widget.destroy();
|
|
*
|
|
* That will kill the widget in a clean way and erase its content from the dom.
|
|
*/
|
|
instance.web.Widget = instance.web.Controller.extend({
|
|
// 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.
|
|
*
|
|
* @type string
|
|
*/
|
|
template: null,
|
|
/**
|
|
* Constructs the widget and sets its parent if a parent is given.
|
|
*
|
|
* @constructs instance.web.Widget
|
|
*
|
|
* @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.
|
|
*/
|
|
init: function(parent) {
|
|
this._super(parent);
|
|
// Bind on_/do_* methods to this
|
|
// We might remove this automatic binding in the future
|
|
for (var name in this) {
|
|
if(typeof(this[name]) == "function") {
|
|
if((/^on_|^do_/).test(name)) {
|
|
this[name] = this[name].bind(this);
|
|
}
|
|
}
|
|
}
|
|
// FIXME: this should not be
|
|
this.setElement(this._make_descriptive());
|
|
this.session = instance.session;
|
|
},
|
|
/**
|
|
* Destroys the current widget, also destroys all its children before destroying itself.
|
|
*/
|
|
destroy: function() {
|
|
_.each(this.getChildren(), function(el) {
|
|
el.destroy();
|
|
});
|
|
if(this.$el) {
|
|
this.$el.remove();
|
|
}
|
|
instance.web.PropertiesMixin.destroy.call(this);
|
|
},
|
|
/**
|
|
* Renders the current widget and appends it to the given jQuery object or Widget.
|
|
*
|
|
* @param target A jQuery object or a Widget instance.
|
|
*/
|
|
appendTo: function(target) {
|
|
var self = this;
|
|
return this.__widgetRenderAndInsert(function(t) {
|
|
self.$el.appendTo(t);
|
|
}, target);
|
|
},
|
|
/**
|
|
* Renders the current widget and prepends it to the given jQuery object or Widget.
|
|
*
|
|
* @param target A jQuery object or a Widget instance.
|
|
*/
|
|
prependTo: function(target) {
|
|
var self = this;
|
|
return this.__widgetRenderAndInsert(function(t) {
|
|
self.$el.prependTo(t);
|
|
}, target);
|
|
},
|
|
/**
|
|
* Renders the current widget and inserts it after to the given jQuery object or Widget.
|
|
*
|
|
* @param target A jQuery object or a Widget instance.
|
|
*/
|
|
insertAfter: function(target) {
|
|
var self = this;
|
|
return this.__widgetRenderAndInsert(function(t) {
|
|
self.$el.insertAfter(t);
|
|
}, target);
|
|
},
|
|
/**
|
|
* Renders the current widget and inserts it before to the given jQuery object or Widget.
|
|
*
|
|
* @param target A jQuery object or a Widget instance.
|
|
*/
|
|
insertBefore: function(target) {
|
|
var self = this;
|
|
return this.__widgetRenderAndInsert(function(t) {
|
|
self.$el.insertBefore(t);
|
|
}, target);
|
|
},
|
|
/**
|
|
* Renders the current widget and replaces the given jQuery object.
|
|
*
|
|
* @param target A jQuery object or a Widget instance.
|
|
*/
|
|
replace: function(target) {
|
|
return this.__widgetRenderAndInsert(_.bind(function(t) {
|
|
this.$el.replaceAll(t);
|
|
}, this), target);
|
|
},
|
|
__widgetRenderAndInsert: function(insertion, target) {
|
|
this.renderElement();
|
|
insertion(target);
|
|
return this.start();
|
|
},
|
|
/**
|
|
* This is the method to implement to render the Widget.
|
|
*/
|
|
renderElement: function() {
|
|
},
|
|
/**
|
|
* Method called after rendering. Mostly used to bind actions, perform asynchronous
|
|
* calls, etc...
|
|
*
|
|
* By convention, the method should return a promise to inform the caller when
|
|
* this widget has been initialized.
|
|
*
|
|
* @returns {jQuery.Deferred}
|
|
*/
|
|
start: function() {
|
|
return $.when();
|
|
},
|
|
/**
|
|
* Renders the element. The default implementation renders the widget using QWeb,
|
|
* `this.template` must be defined. The context given to QWeb contains the "widget"
|
|
* key that references `this`.
|
|
*/
|
|
renderElement: function() {
|
|
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;
|
|
},
|
|
/**
|
|
* 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
|
|
*/
|
|
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);
|
|
}
|
|
});
|
|
|
|
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.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
|
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
|
|
*
|
|
* @param {String} [server] JSON-RPC endpoint hostname
|
|
* @param {String} [port] JSON-RPC endpoint port
|
|
*/
|
|
init: function() {
|
|
instance.web.PropertiesMixin.init.call(this);
|
|
this.server = null;
|
|
this.debug = ($.deparam($.param.querystring()).debug != undefined);
|
|
},
|
|
setup: function(origin) {
|
|
var window_origin = location.protocol+"//"+location.host, self=this;
|
|
this.origin = origin ? _.str.rtrim(origin,'/') : window_origin;
|
|
this.prefix = this.origin;
|
|
this.server = this.origin; // keep chs happy
|
|
this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
|
|
},
|
|
/**
|
|
* Executes an RPC call, registering the provided callbacks.
|
|
*
|
|
* Registers a default error callback if none is provided, and handles
|
|
* setting the correct session id and session context in the parameter
|
|
* objects
|
|
*
|
|
* @param {String} url RPC endpoint
|
|
* @param {Object} params call parameters
|
|
* @param {Object} options additional options for rpc call
|
|
* @param {Function} success_callback function to execute on RPC call success
|
|
* @param {Function} error_callback function to execute on RPC call failure
|
|
* @returns {jQuery.Deferred} jquery-provided ajax deferred
|
|
*/
|
|
rpc: function(url, params, options) {
|
|
var self = this;
|
|
options = options || {};
|
|
// url can be an $.ajax option object
|
|
if (_.isString(url)) {
|
|
url = { url: url };
|
|
}
|
|
_.defaults(params, {
|
|
context: this.user_context || {}
|
|
});
|
|
// Construct a JSON-RPC2 request, method is currently unused
|
|
if (this.debug)
|
|
params.debug = 1;
|
|
var payload = {
|
|
jsonrpc: '2.0',
|
|
method: 'call',
|
|
params: params,
|
|
id: _.uniqueId('r')
|
|
};
|
|
var deferred = $.Deferred();
|
|
if (! options.shadow)
|
|
this.trigger('request', url, payload);
|
|
|
|
this.rpc_function(url, payload).then(
|
|
function (response, textStatus, jqXHR) {
|
|
if (! options.shadow)
|
|
self.trigger('response', response);
|
|
if (!response.error) {
|
|
deferred.resolve(response["result"], textStatus, jqXHR);
|
|
} else if (response.error.data.type === "session_invalid") {
|
|
self.uid = false;
|
|
} else {
|
|
deferred.reject(response.error, $.Event());
|
|
}
|
|
},
|
|
function(jqXHR, textStatus, errorThrown) {
|
|
if (! options.shadow)
|
|
self.trigger('response_failed', jqXHR);
|
|
var error = {
|
|
code: -32098,
|
|
message: "XmlHttpRequestError " + errorThrown,
|
|
data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
|
|
};
|
|
deferred.reject(error, $.Event());
|
|
});
|
|
// Allow deferred user to disable rpc_error call in fail
|
|
deferred.fail(function() {
|
|
deferred.fail(function(error, event) {
|
|
if (!event.isDefaultPrevented()) {
|
|
self.trigger('error', error, event);
|
|
}
|
|
});
|
|
});
|
|
return deferred;
|
|
},
|
|
/**
|
|
* Raw JSON-RPC call
|
|
*
|
|
* @returns {jQuery.Deferred} ajax-webd deferred object
|
|
*/
|
|
rpc_json: function(url, payload) {
|
|
var self = this;
|
|
var ajax = _.extend({
|
|
type: "POST",
|
|
dataType: 'json',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify(payload),
|
|
processData: false
|
|
}, url);
|
|
if (this.synch)
|
|
ajax.async = false;
|
|
return $.ajax(ajax);
|
|
},
|
|
rpc_jsonp: function(url, payload) {
|
|
var self = this;
|
|
// extracted from payload to set on the url
|
|
var data = {
|
|
session_id: this.session_id,
|
|
id: payload.id,
|
|
sid: this.httpsessionid,
|
|
};
|
|
|
|
var set_sid = function (response, textStatus, jqXHR) {
|
|
// If response give us the http session id, we store it for next requests...
|
|
if (response.httpsessionid) {
|
|
self.httpsessionid = response.httpsessionid;
|
|
}
|
|
};
|
|
|
|
url.url = this.url(url.url, null);
|
|
var ajax = _.extend({
|
|
type: "GET",
|
|
dataType: 'jsonp',
|
|
jsonp: 'jsonp',
|
|
cache: false,
|
|
data: data
|
|
}, url);
|
|
if (this.synch)
|
|
ajax.async = false;
|
|
var payload_str = JSON.stringify(payload);
|
|
var payload_url = $.param({r:payload_str});
|
|
if(payload_url.length < 2000) {
|
|
// Direct jsonp request
|
|
ajax.data.r = payload_str;
|
|
return $.ajax(ajax).done(set_sid);
|
|
} else {
|
|
// Indirect jsonp request
|
|
var ifid = _.uniqueId('oe_rpc_iframe');
|
|
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 + '?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").remove();
|
|
}
|
|
$form.remove();
|
|
};
|
|
var deferred = $.Deferred();
|
|
// the first bind is fired up when the iframe is added to the DOM
|
|
$iframe.bind('load', function() {
|
|
// the second bind is fired up when the result of the form submission is received
|
|
$iframe.unbind('load').bind('load', function() {
|
|
$.ajax(ajax).always(function() {
|
|
cleanUp();
|
|
}).done(function() {
|
|
deferred.resolve.apply(deferred, arguments);
|
|
}).fail(function() {
|
|
deferred.reject.apply(deferred, arguments);
|
|
});
|
|
});
|
|
// now that the iframe can receive data, we fill and submit the form
|
|
$form.submit();
|
|
});
|
|
// append the iframe to the DOM (will trigger the first load)
|
|
$form.after($iframe);
|
|
return deferred.done(set_sid);
|
|
}
|
|
},
|
|
|
|
url: function(path, params) {
|
|
var qs = '';
|
|
if (!_.isNull(params)) {
|
|
params = _.extend(params || {}, {session_id: this.session_id});
|
|
if (this.httpsessionid) {
|
|
params.sid = this.httpsessionid;
|
|
}
|
|
qs = '?' + $.param(params);
|
|
}
|
|
return this.prefix + path + qs;
|
|
},
|
|
});
|
|
|
|
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:
|