diff options
| author | Christophe Matthieu <chm@openerp.com> | 2013-08-01 16:42:29 +0200 |
|---|---|---|
| committer | Christophe Matthieu <chm@openerp.com> | 2013-08-01 16:42:29 +0200 |
| commit | 55e3a0dedd98b512ec6f649d5ff645add53c38ab (patch) | |
| tree | eb560d79393be4f6329bc3630bce12b11177c456 /addons/web/static/src | |
| parent | 25f117a0561cef866f21c5360cd09ab32066b96b (diff) | |
| parent | be7b1239c33c24f2727216eebf1d5f9907c54046 (diff) | |
[MERGE] from trunk-website-public_uid-chm
bzr revid: chm@openerp.com-20130801144229-tibuoa574kn86kyy
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/js/boot.js | 75 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 781 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 18 | ||||
| -rw-r--r-- | addons/web/static/src/js/openerpframework.js | 783 | ||||
| -rw-r--r-- | addons/web/static/src/js/testing.js | 44 |
5 files changed, 861 insertions, 840 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 4565d220dc8..87115fd30ea 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -7,15 +7,20 @@ * @namespace openerp */ (function() { - if (this.openerp) - return; - var session_counter = 0; + var inited = false; - var openerp = this.openerp = { + _.extend(openerp, { // Per session namespace // openerp.<module> will map to // openerp.instances.sessionname.<module> using a closure - instances: {}, + instances: {instance0: openerp}, + // links to the global openerp + _openerp: openerp, + // this unique id will be replaced by hostname_databasename by + // openerp.web.Session on the first connection + _session_id: "instance0", + _modules: ['web'], + web_mobile: {}, /** * OpenERP instance constructor * @@ -24,41 +29,43 @@ init: function(modules) { if (modules === null) { modules = []; - } else { - modules = _.union(['web'], modules || []); } - var new_instance = { - // links to the global openerp - _openerp: openerp, - // this unique id will be replaced by hostname_databasename by - // openerp.web.Session on the first connection - _session_id: "instance" + session_counter++, - _modules: modules, - web: {}, - web_mobile: {} - }; - openerp.instances[new_instance._session_id] = new_instance; + if (inited) + throw new Error("OpenERP was already inited"); + inited = true; + init_web_modules(); for(var i=0; i < modules.length; i++) { - new_instance[modules[i]] = {}; - if (openerp[modules[i]]) { - openerp[modules[i]](new_instance,new_instance[modules[i]]); + if (modules[i] === "web") + continue; + var fct = openerp[modules[i]]; + if (typeof(fct) === "function") { + openerp[modules[i]] = {}; + for (var k in fct) { + openerp[modules[i]][k] = fct[k]; + } + fct(openerp, openerp[modules[i]]); } } - return new_instance; + openerp._modules = ['web'].concat(modules); + return openerp; } - }; -})(); + }); -/*--------------------------------------------------------- - * OpenERP Web web module split - *---------------------------------------------------------*/ -openerp.web = function(session) { - var files = ["pyeval", "corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; - for(var i=0; i<files.length; i++) { - if(openerp.web[files[i]]) { - openerp.web[files[i]](session); + /*--------------------------------------------------------- + * OpenERP Web web module split + *---------------------------------------------------------*/ + function init_web_modules() { + var files = ["pyeval", "corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; + for(var i=0; i<files.length; i++) { + var fct = openerp.web[files[i]]; + if(typeof(fct) === "function") { + openerp.web[files[i]] = {}; + for (var k in fct) { + openerp.web[files[i]][k] = fct[k]; + } + fct(openerp, openerp.web[files[i]]); + } } } -}; +})(); -// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 6ab7a30c642..c419685c222 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -25,486 +25,7 @@ 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 - _.each(prop, function(val, name) { - // 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"); - } - // 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) { - _.each(properties, function(val, name) { - 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; - }, - /** - Utility method to only execute asynchronous actions if the current - object has not been destroyed. - - @param {$.Deferred} promise The promise representing the asynchronous - action. - @param {bool} [reject=false] If true, the returned promise will be - rejected with no arguments if the current - object is destroyed. If false, the - returned promise will never be resolved - or rejected. - @returns {$.Deferred} A promise that will mirror the given promise if - everything goes fine but will either be rejected - with no arguments or never resolved if the - current object is destroyed. - */ - alive: function(promise, reject) { - var def = $.Deferred(); - var self = this; - promise.done(function() { - if (! self.isDestroyed()) { - if (! reject) - def.resolve.apply(def, arguments); - else - def.reject(); - } - }).fail(function() { - if (! self.isDestroyed()) { - if (! reject) - def.reject.apply(def, arguments); - else - def.reject(); - } - }); - return def.promise(); - }, - /** - * 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); - }; - }, +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. @@ -530,300 +51,34 @@ instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin return false; }, rpc: function(url, data, options) { - return this.alive(instance.session.rpc(url, data, options)); + return this.alive(openerp.session.rpc(url, data, options)); } -}); +}; /** - * 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, + 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 widget and sets its parent if a parent is given. - * - * @constructs instance.web.Widget + * Constructs the object and sets its parent if a parent is given. * - * @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 + * @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) { - 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(); - }, - /** - * Method called after rendering. Mostly used to bind actions, perform asynchronous - * calls, etc... - * - * By convention, this method should return an object that can be passed to $.when() - * to inform the caller when this widget has been initialized. - * - * @returns {jQuery.Deferred or any} - */ - 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)); + openerp.web.PropertiesMixin.init.call(this); + this.setParent(parent); + this.session = openerp.session; }, - 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'); +openerp.web.Widget.include(_.extend({}, ControllerMixin, { + init: function() { + this._super.apply(this, arguments); + this.session = openerp.session; }, - /** - * 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({ /** diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 8eea24345d2..c8deca1c9ac 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -250,10 +250,15 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess continue; instance[mod] = {}; // init module mod - if(instance._openerp[mod] !== undefined) { - instance._openerp[mod](instance,instance[mod]); - this.module_loaded[mod] = true; + 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; } }, /** @@ -559,14 +564,11 @@ instance.web._t = new instance.web.TranslationDataBase().build_translation_funct instance.web._lt = function (s) { return {toString: function () { return instance.web._t(s); }}; }; -instance.web.qweb = new QWeb2.Engine(); instance.web.qweb.debug = instance.session.debug; -instance.web.qweb.default_dict = { - '_' : _, +_.extend(instance.web.qweb.default_dict, { '_t' : instance.web._t, - 'JSON': JSON, '__debug__': instance.session.debug, -}; +}); instance.web.qweb.preprocess_node = function() { // Note that 'this' is the Qweb Node switch (this.node.nodeType) { diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js new file mode 100644 index 00000000000..374493b1f7e --- /dev/null +++ b/addons/web/static/src/js/openerpframework.js @@ -0,0 +1,783 @@ + +(function() { +/* jshint es3: true */ +"use strict"; + +function declare($, _, QWeb2) { +var openerp = {}; +openerp.web = {}; + +/** + * 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 = openerp.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) + openerp.web.Class = function(){}; + + /** + * Subclass an existing class + * + * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class + */ + openerp.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 This = this; + var prototype = new This(); + initializing = false; + + // Copy the properties over onto the new prototype + _.each(prop, function(val, name) { + // 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 !== openerp.web.Class){ + throw new Error("You can only instanciate objects with the 'new' operator"); + } + // 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) { + _.each(properties, function(val, name) { + 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 = this.extend; + + 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. + */ +openerp.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; + }, + /** + Utility method to only execute asynchronous actions if the current + object has not been destroyed. + + @param {$.Deferred} promise The promise representing the asynchronous + action. + @param {bool} [reject=false] If true, the returned promise will be + rejected with no arguments if the current + object is destroyed. If false, the + returned promise will never be resolved + or rejected. + @returns {$.Deferred} A promise that will mirror the given promise if + everything goes fine but will either be rejected + with no arguments or never resolved if the + current object is destroyed. + */ + alive: function(promise, reject) { + var def = $.Deferred(); + var self = this; + promise.done(function() { + if (! self.isDestroyed()) { + if (! reject) + def.resolve.apply(def, arguments); + else + def.reject(); + } + }).fail(function() { + if (! self.isDestroyed()) { + if (! reject) + def.reject.apply(def, arguments); + else + def.reject(); + } + }); + return def.promise(); + }, + /** + * 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 = openerp.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; + } +}); + +openerp.web.EventDispatcherMixin = _.extend({}, openerp.web.ParentedMixin, { + __eventDispatcherMixin: true, + init: function() { + openerp.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(); + openerp.web.ParentedMixin.destroy.call(this); + } +}); + +openerp.web.PropertiesMixin = _.extend({}, openerp.web.EventDispatcherMixin, { + init: function() { + openerp.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]; + } +}); + +/** + * 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 = openerp.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. + */ +openerp.web.Widget = openerp.web.Class.extend(openerp.web.PropertiesMixin, { + // 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 openerp.web.Widget + * + * @param {openerp.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) { + openerp.web.PropertiesMixin.init.call(this); + this.setParent(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()); + }, + /** + * 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(); + } + openerp.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(); + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, this method should return an object that can be passed to $.when() + * to inform the caller when this widget has been initialized. + * + * @returns {jQuery.Deferred or any} + */ + 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(openerp.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); + }, + /** + * 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); + }; + } +}); + +openerp.web.qweb = new QWeb2.Engine(); + +openerp.web.qweb.default_dict = { + '_' : _, + 'JSON': JSON +}; + +openerp.declare = declare; + +return openerp; +} + +if (typeof(define) !== "undefined") { // amd + define(["jquery", "underscore", "qweb2"], declare); +} else { + window.openerp = declare($, _, QWeb2); +} + +})();
\ No newline at end of file diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index ed26238028e..880da9f1bc1 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -164,6 +164,8 @@ openerp.testing = {}; }); }; + var openerp_inited = false; + var db = window['oe_db_info']; testing.section = function (name, options, body) { if (_.isFunction(options)) { @@ -193,8 +195,7 @@ openerp.testing = {}; 0, module_index + 1 || undefined); // Serialize options for this precise test case - // WARNING: typo is from jquery, do not fix! - var env = QUnit.config.currentModuleTestEnviroment; + var env = QUnit.config.currentModuleTestEnvironment; // section setup // case setup // test @@ -240,40 +241,13 @@ openerp.testing = {}; } QUnit.test(name, function () { - var instance; - if (!opts.dependencies) { - instance = openerp.init(module_deps); - } else { - // empty-but-specified dependencies actually allow running - // without loading any module into the instance - - // TODO: clean up this mess - var d = opts.dependencies.slice(); - // dependencies list should be in deps order, reverse to make - // loading order from last - d.reverse(); - var di = 0; - while (di < d.length) { - var m = /^web\.(\w+)$/.exec(d[di]); - if (m) { - d[di] = m[1]; - } - d.splice.apply(d, [di+1, 0].concat( - _(dependencies[d[di]]).reverse())); - ++di; - } - - instance = openerp.init(null); - _(d).chain() - .reverse() - .uniq() - .each(function (module) { - openerp.web[module](instance); - }); - } - if (instance.session) { - instance.session.uid = 42; + var instance = openerp; + if (!openerp_inited) { + openerp.init(module_deps); + openerp_inited = true; } + instance.session = new instance.web.Session(); + instance.session.uid = 42; if (_.isNumber(opts.asserts)) { expect(opts.asserts); } |
