[MERGE] backbonify-widgets, minor api changes that make Widget more similar to backbone Views
bzr revid: al@openerp.com-20120801124804-ov9l9ytabqx54gdo
This commit is contained in:
@@ -57,9 +57,7 @@ instance.web.Dialog = instance.web.Widget.extend({
|
||||
init: function (parent, options, content) {
|
||||
var self = this;
|
||||
this._super(parent);
|
||||
if (content) {
|
||||
this.$element = content instanceof $ ? content : $(content);
|
||||
}
|
||||
this.setElement(content || this.make(this.tagName));
|
||||
this.dialog_options = {
|
||||
modal: true,
|
||||
destroy_on_close: true,
|
||||
@@ -589,6 +587,12 @@ instance.web.Login = instance.web.Widget.extend({
|
||||
self.$(".oe_login_pane").fadeIn("fast");
|
||||
self.$element.addClass("oe_login_invalid");
|
||||
});
|
||||
},
|
||||
show: function () {
|
||||
this.$element.show();
|
||||
},
|
||||
hide: function () {
|
||||
this.$element.hide();
|
||||
}
|
||||
});
|
||||
instance.web.client_actions.add("login", "instance.web.Login");
|
||||
@@ -880,12 +884,11 @@ instance.web.Client = instance.web.Widget.extend({
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
return instance.connection.session_bind(this.origin).then(function() {
|
||||
return instance.connection.session_bind(this.origin).pipe(function() {
|
||||
var $e = $(QWeb.render(self._template, {}));
|
||||
self.$element.replaceWith($e);
|
||||
self.$element = $e;
|
||||
self.replaceElement($e);
|
||||
self.bind_events();
|
||||
self.show_common();
|
||||
return self.show_common();
|
||||
});
|
||||
},
|
||||
bind_events: function() {
|
||||
|
||||
@@ -491,23 +491,19 @@ 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.
|
||||
*
|
||||
@@ -517,14 +513,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);
|
||||
},
|
||||
/**
|
||||
@@ -534,7 +525,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
|
||||
_.each(this.getChildren(), function(el) {
|
||||
el.destroy();
|
||||
});
|
||||
if(this.$element != null) {
|
||||
if(this.$element) {
|
||||
this.$element.remove();
|
||||
}
|
||||
instance.web.PropertiesMixin.destroy.call(this);
|
||||
@@ -613,6 +604,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
|
||||
* @returns {jQuery.Deferred}
|
||||
*/
|
||||
start: function() {
|
||||
return $.when();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -673,6 +665,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.
|
||||
@@ -689,13 +687,11 @@ 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);
|
||||
// FIXME: this should not be
|
||||
this.setElement(this._make_descriptive());
|
||||
this.session = instance.connection;
|
||||
},
|
||||
/**
|
||||
@@ -704,20 +700,120 @@ 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.$element;
|
||||
this.setElement($el);
|
||||
if ($oldel && !$oldel.is(this.$element)) {
|
||||
$oldel.replaceWith(this.$element);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
/**
|
||||
* Shortcut for $element.find() like backbone
|
||||
* Re-sets the widget's root element (el/$el/$element).
|
||||
*
|
||||
* 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 $element
|
||||
// always
|
||||
if (this.$element) {
|
||||
this.undelegateEvents();
|
||||
}
|
||||
|
||||
this.$element = (element instanceof $) ? element : $(element);
|
||||
this.el = this.$element[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.$element.on(event, method);
|
||||
} else {
|
||||
this.$element.on(event, selector, method);
|
||||
}
|
||||
}
|
||||
},
|
||||
undelegateEvents: function () {
|
||||
this.$element.off('.widget_events');
|
||||
},
|
||||
/**
|
||||
* Shortcut for ``this.$element.find(selector)``
|
||||
*
|
||||
* @param {String} selector CSS selector, rooted in $el
|
||||
* @returns {jQuery} selector match
|
||||
*/
|
||||
$: function(selector) {
|
||||
return this.$element.find(selector);
|
||||
},
|
||||
/**
|
||||
* Informs the action manager to do an action. This supposes that
|
||||
|
||||
@@ -19,15 +19,14 @@ instance.web.OldWidget = instance.web.Widget.extend({
|
||||
this._super(parent);
|
||||
this.element_id = element_id;
|
||||
this.element_id = this.element_id || _.uniqueId('widget-');
|
||||
|
||||
var tmp = document.getElementById(this.element_id);
|
||||
this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName));
|
||||
this.setElement(tmp || this._make_descriptive());
|
||||
},
|
||||
renderElement: function() {
|
||||
var rendered = this.render();
|
||||
if (rendered) {
|
||||
var elem = $(rendered);
|
||||
this.$element.replaceWith(elem);
|
||||
this.$element = elem;
|
||||
this.replaceElement($(rendered));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -861,13 +861,13 @@ instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web
|
||||
);
|
||||
}
|
||||
});
|
||||
instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{
|
||||
instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{
|
||||
template: null,
|
||||
/**
|
||||
* Root class of all search widgets
|
||||
*
|
||||
* @constructs instance.web.search.Widget
|
||||
* @extends instance.web.OldWidget
|
||||
* @extends instance.web.Widget
|
||||
*
|
||||
* @param view the ancestor view of this widget
|
||||
*/
|
||||
|
||||
@@ -101,7 +101,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||
w.off('focused blurred');
|
||||
w.destroy();
|
||||
});
|
||||
this.$element.off('.formBlur');
|
||||
if (this.$element) {
|
||||
this.$element.off('.formBlur');
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
on_loaded: function(data) {
|
||||
@@ -3076,7 +3078,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
|
||||
this.views = views;
|
||||
|
||||
this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
|
||||
this.viewmanager.$element.addClass("oe_view_manager_one2many");
|
||||
this.viewmanager.o2m = self;
|
||||
var once = $.Deferred().then(function() {
|
||||
self.init_form_last_update.resolve();
|
||||
@@ -4304,7 +4305,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan
|
||||
this.selection.view = this.view;
|
||||
this.selection.set({force_readonly: this.get('effective_readonly')});
|
||||
this.selection.on("change:value", this, this.on_selection_changed);
|
||||
this.selection.$element = $(".oe_form_view_reference_selection", this.$element);
|
||||
this.selection.setElement(this.$(".oe_form_view_reference_selection"));
|
||||
this.selection.renderElement();
|
||||
this.selection.start();
|
||||
this.selection
|
||||
@@ -4317,7 +4318,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan
|
||||
this.m2o.view = this.view;
|
||||
this.m2o.set({force_readonly: this.get("effective_readonly")});
|
||||
this.m2o.on("change:value", this, this.data_changed);
|
||||
this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element);
|
||||
this.m2o.setElement(this.$(".oe_form_view_reference_m2o"));
|
||||
this.m2o.renderElement();
|
||||
this.m2o.start();
|
||||
this.m2o
|
||||
|
||||
@@ -96,9 +96,6 @@ openerp.web.list_editable = function (instance) {
|
||||
},
|
||||
on_loaded: function (data, grouped) {
|
||||
var self = this;
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
// tree/@editable takes priority on everything else if present.
|
||||
var result = this._super(data, grouped);
|
||||
if (this.editable()) {
|
||||
@@ -118,6 +115,7 @@ openerp.web.list_editable = function (instance) {
|
||||
self.start_edition();
|
||||
}
|
||||
});
|
||||
this.editor.destroy();
|
||||
// Editor is not restartable due to formview not being
|
||||
// restartable
|
||||
this.editor = this.make_editor();
|
||||
|
||||
@@ -423,7 +423,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
||||
.find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
|
||||
.parent().addClass('active');
|
||||
|
||||
$.when(view_promise).then(function () {
|
||||
return $.when(view_promise).then(function () {
|
||||
_.each(_.keys(self.views), function(view_name) {
|
||||
var controller = self.views[view_name].controller;
|
||||
if (controller) {
|
||||
@@ -445,7 +445,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
||||
}
|
||||
});
|
||||
});
|
||||
return view_promise;
|
||||
},
|
||||
do_create_view: function(view_type) {
|
||||
// Lazy loading of views
|
||||
@@ -1175,7 +1174,7 @@ instance.web.View = instance.web.Widget.extend({
|
||||
this.view_id = view_id;
|
||||
this.set_default_options(options);
|
||||
},
|
||||
start: function() {
|
||||
start: function () {
|
||||
return this.load_view();
|
||||
},
|
||||
load_view: function() {
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
$(document).ready(function () {
|
||||
var $fix = $('#qunit-fixture');
|
||||
var mod = {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
|
||||
instance.web.qweb = new QWeb2.Engine();
|
||||
instance.web.qweb.add_template(
|
||||
'<no>' +
|
||||
'<t t-name="test.widget.template">' +
|
||||
'<ol>' +
|
||||
'<li t-foreach="5" t-as="counter" ' +
|
||||
't-attf-class="class-#{counter}">' +
|
||||
'<input/>' +
|
||||
'<t t-esc="counter"/>' +
|
||||
'</li>' +
|
||||
'</ol>' +
|
||||
'</t>' +
|
||||
'<t t-name="test.widget.template-value">' +
|
||||
'<p><t t-esc="widget.value"/></p>' +
|
||||
'</t>' +
|
||||
'</no>');
|
||||
}
|
||||
};
|
||||
var instance;
|
||||
|
||||
module('Widget.proxy', mod);
|
||||
test('(String)', function () {
|
||||
var W = instance.web.Widget.extend({
|
||||
exec: function () {
|
||||
this.executed = true;
|
||||
}
|
||||
});
|
||||
var w = new W;
|
||||
var fn = w.proxy('exec');
|
||||
fn();
|
||||
ok(w.executed, 'should execute the named method in the right context');
|
||||
});
|
||||
test('(String)(*args)', function () {
|
||||
var W = instance.web.Widget.extend({
|
||||
exec: function (arg) {
|
||||
this.executed = arg;
|
||||
}
|
||||
});
|
||||
var w = new W;
|
||||
var fn = w.proxy('exec');
|
||||
fn(42);
|
||||
ok(w.executed, "should execute the named method in the right context");
|
||||
equal(w.executed, 42, "should be passed the proxy's arguments");
|
||||
});
|
||||
test('(String), include', function () {
|
||||
// the proxy function should handle methods being changed on the class
|
||||
// and should always proxy "by name", to the most recent one
|
||||
var W = instance.web.Widget.extend({
|
||||
exec: function () {
|
||||
this.executed = 1;
|
||||
}
|
||||
});
|
||||
var w = new W;
|
||||
var fn = w.proxy('exec');
|
||||
W.include({
|
||||
exec: function () { this.executed = 2; }
|
||||
});
|
||||
|
||||
fn();
|
||||
equal(w.executed, 2, "should be lazily resolved");
|
||||
});
|
||||
|
||||
test('(Function)', function () {
|
||||
var w = new (instance.web.Widget.extend({ }));
|
||||
|
||||
var fn = w.proxy(function () { this.executed = true; });
|
||||
fn();
|
||||
ok(w.executed, "should set the function's context (like Function#bind)");
|
||||
});
|
||||
test('(Function)(*args)', function () {
|
||||
var w = new (instance.web.Widget.extend({ }));
|
||||
|
||||
var fn = w.proxy(function (arg) { this.executed = arg; });
|
||||
fn(42);
|
||||
equal(w.executed, 42, "should be passed the proxy's arguments");
|
||||
});
|
||||
|
||||
module('Widget.renderElement', mod);
|
||||
test('no template, default', function () {
|
||||
var w = new (instance.web.Widget.extend({ }));
|
||||
|
||||
ok(!w.$element, "should not initially have a root element");
|
||||
w.renderElement();
|
||||
ok(w.$element, "should have generated a root element");
|
||||
strictEqual(w.$element, w.$element, "should provide $element alias");
|
||||
ok(w.$element.is(w.el), "should provide raw DOM alias");
|
||||
|
||||
equal(w.el.nodeName, 'DIV', "should have generated the default element");
|
||||
equal(w.el.attributes.length, 0, "should not have generated any attribute");
|
||||
ok(_.isEmpty(w.$element.html(), "should not have generated any content"));
|
||||
});
|
||||
test('no template, custom tag', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
tagName: 'ul'
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
equal(w.el.nodeName, 'UL', "should have generated the custom element tag");
|
||||
});
|
||||
test('no template, @id', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
id: 'foo'
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
equal(w.el.attributes.length, 1, "should have one attribute");
|
||||
equal(w.$element.attr('id'), 'foo', "should have generated the id attribute");
|
||||
equal(w.el.id, 'foo', "should also be available via property");
|
||||
});
|
||||
test('no template, @className', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
className: 'oe_some_class'
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
equal(w.el.className, 'oe_some_class', "should have the right property");
|
||||
equal(w.$element.attr('class'), 'oe_some_class', "should have the right attribute");
|
||||
});
|
||||
test('no template, bunch of attributes', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
attributes: {
|
||||
'id': 'some_id',
|
||||
'class': 'some_class',
|
||||
'data-foo': 'data attribute',
|
||||
'clark': 'gable',
|
||||
'spoiler': 'snape kills dumbledore'
|
||||
}
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
equal(w.el.attributes.length, 5, "should have all the specified attributes");
|
||||
|
||||
equal(w.el.id, 'some_id');
|
||||
equal(w.$element.attr('id'), 'some_id');
|
||||
|
||||
equal(w.el.className, 'some_class');
|
||||
equal(w.$element.attr('class'), 'some_class');
|
||||
|
||||
equal(w.$element.attr('data-foo'), 'data attribute');
|
||||
equal(w.$element.data('foo'), 'data attribute');
|
||||
|
||||
equal(w.$element.attr('clark'), 'gable');
|
||||
equal(w.$element.attr('spoiler'), 'snape kills dumbledore');
|
||||
});
|
||||
|
||||
test('template', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
equal(w.el.nodeName, 'OL');
|
||||
equal(w.$element.children().length, 5);
|
||||
equal(w.el.textContent, '01234');
|
||||
});
|
||||
|
||||
module('Widget.$', mod);
|
||||
test('basic-alias', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
ok(w.$('li:eq(3)').is(w.$element.find('li:eq(3)')),
|
||||
"should do the same thing as calling find on the widget root");
|
||||
});
|
||||
|
||||
module('Widget.events', mod);
|
||||
test('delegate', function () {
|
||||
var a = [];
|
||||
var w = new (instance.web.Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
events: {
|
||||
'click': function () {
|
||||
a[0] = true;
|
||||
strictEqual(this, w, "should trigger events in widget")
|
||||
},
|
||||
'click li.class-3': 'class3',
|
||||
'change input': function () { a[2] = true; }
|
||||
},
|
||||
class3: function () { a[1] = true; }
|
||||
}));
|
||||
w.renderElement();
|
||||
|
||||
w.$element.click();
|
||||
w.$('li:eq(3)').click();
|
||||
w.$('input:last').val('foo').change();
|
||||
|
||||
for(var i=0; i<3; ++i) {
|
||||
ok(a[i], "should pass test " + i);
|
||||
}
|
||||
});
|
||||
test('undelegate', function () {
|
||||
var clicked = false, newclicked = false;
|
||||
var w = new (instance.web.Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
events: { 'click li': function () { clicked = true; } }
|
||||
}));
|
||||
w.renderElement();
|
||||
w.$element.on('click', 'li', function () { newclicked = true });
|
||||
|
||||
w.$('li').click();
|
||||
ok(clicked, "should trigger bound events");
|
||||
ok(newclicked, "should trigger bound events");
|
||||
clicked = newclicked = false;
|
||||
|
||||
w.undelegateEvents();
|
||||
w.$('li').click();
|
||||
ok(!clicked, "undelegate should unbind events delegated");
|
||||
ok(newclicked, "undelegate should only unbind events it created");
|
||||
});
|
||||
|
||||
module('Widget.renderElement', mod);
|
||||
test('repeated', function () {
|
||||
var w = new (instance.web.Widget.extend({
|
||||
template: 'test.widget.template-value'
|
||||
}));
|
||||
w.value = 42;
|
||||
w.appendTo($fix)
|
||||
.always(start)
|
||||
.done(function () {
|
||||
equal($fix.find('p').text(), '42', "DOM fixture should contain initial value");
|
||||
equal(w.$element.text(), '42', "should set initial value");
|
||||
w.value = 36;
|
||||
w.renderElement();
|
||||
equal($fix.find('p').text(), '36', "DOM fixture should use new value");
|
||||
equal(w.$element.text(), '36', "should set new value");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,5 +61,6 @@
|
||||
<script type="text/javascript" src="/web/static/test/rpc.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/evals.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/search.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/Widget.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/list-editable.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -492,15 +492,17 @@ instance.web_calendar.Sidebar = instance.web.Widget.extend({
|
||||
}
|
||||
});
|
||||
instance.web_calendar.SidebarFilter = instance.web.Widget.extend({
|
||||
events: {
|
||||
'change input:checkbox': 'on_filter_click'
|
||||
},
|
||||
init: function(parent, view) {
|
||||
this._super(parent);
|
||||
this.view = view;
|
||||
this.$element.delegate('input:checkbox', 'change', this.on_filter_click);
|
||||
},
|
||||
on_events_loaded: function(filters) {
|
||||
var selected_filters = this.view.selected_filters.slice(0);
|
||||
this.$element.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters }));
|
||||
this.$element.find('div.oe_calendar_responsible input').each(function() {
|
||||
this.$('div.oe_calendar_responsible input').each(function() {
|
||||
if (_.indexOf(selected_filters, $(this).val()) > -1) {
|
||||
$(this).click();
|
||||
}
|
||||
@@ -511,7 +513,7 @@ instance.web_calendar.SidebarFilter = instance.web.Widget.extend({
|
||||
responsibles = [],
|
||||
$e = $(e.target);
|
||||
this.view.selected_filters = [];
|
||||
this.$element.find('div.oe_calendar_responsible input:checked').each(function() {
|
||||
this.$('div.oe_calendar_responsible input:checked').each(function() {
|
||||
responsibles.push($(this).val());
|
||||
self.view.selected_filters.push($(this).val());
|
||||
});
|
||||
|
||||
@@ -226,6 +226,8 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
|
||||
}
|
||||
},
|
||||
renderElement: function() {
|
||||
this._super();
|
||||
|
||||
var check = _.detect(this.node.children, function(column, column_index) {
|
||||
return _.detect(column.children,function(element){
|
||||
return element.tag === "action"? element: false;
|
||||
|
||||
@@ -720,9 +720,7 @@ instance.web_kanban.KanbanRecord = instance.web.OldWidget.extend({
|
||||
this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).then(function(records) {
|
||||
if (records.length) {
|
||||
self.set_record(records[0]);
|
||||
var $render = $(self.render());
|
||||
self.$element.replaceWith($render);
|
||||
self.$element = $render;
|
||||
this.replaceElement($(self.render()));
|
||||
self.$element.data('widget', self);
|
||||
self.bind_events();
|
||||
self.group.compute_cards_auto_height();
|
||||
|
||||
@@ -48,8 +48,7 @@ instance.web_mobile.Login = instance.web.OldWidget.extend({
|
||||
jQuery("#oe_header").children().remove();
|
||||
this.rpc("/web/database/get_list", {}, function(result) {
|
||||
self.db_list = result.db_list;
|
||||
$('#'+self.element_id).html(self.render(self));
|
||||
self.$element = $('#'+self.element_id);
|
||||
this.setElement($('#'+self.element_id).html(self.render(self)));
|
||||
if(self.session.db!=""){
|
||||
self.$element.find("#database").val(self.session.db);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ openerp.web_process = function (instance) {
|
||||
_t = instance.web._t;
|
||||
instance.web.ViewManager.include({
|
||||
start: function() {
|
||||
this._super();
|
||||
var _super = this._super();
|
||||
this.process_check();
|
||||
this.process_help = this.action ? this.action.help : 'Help: Not Defined';
|
||||
this.model = this.dataset.model;
|
||||
if(this.action) this.process_model = this.action.res_model;
|
||||
else this.process_model = this.model;
|
||||
return _super;
|
||||
},
|
||||
process_check: function() {
|
||||
var self = this,
|
||||
|
||||
-109
@@ -113,103 +113,6 @@ initializing the addon.
|
||||
Creating new standard roles
|
||||
---------------------------
|
||||
|
||||
Widget
|
||||
++++++
|
||||
|
||||
This is the base class for all visual components. It provides a number of
|
||||
services for the management of a DOM subtree:
|
||||
|
||||
* Rendering with QWeb
|
||||
|
||||
* Parenting-child relations
|
||||
|
||||
* Life-cycle management (including facilitating children destruction when a
|
||||
parent object is removed)
|
||||
|
||||
* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
|
||||
be anything the corresponding jQuery method accepts (generally selectors,
|
||||
DOM nodes and jQuery objects):
|
||||
|
||||
:js:func:`~openerp.base.Widget.appendTo`
|
||||
Renders the widget and inserts it as the last child of the target, uses
|
||||
`.appendTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.prependTo`
|
||||
Renders the widget and inserts it as the first child of the target, uses
|
||||
`.prependTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertAfter`
|
||||
Renders the widget and inserts it as the preceding sibling of the target,
|
||||
uses `.insertAfter()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertBefore`
|
||||
Renders the widget and inserts it as the following sibling of the target,
|
||||
uses `.insertBefore()`_
|
||||
|
||||
:js:class:`~openerp.base.Widget` inherits from
|
||||
:js:class:`~openerp.base.SessionAware`, so subclasses can easily access the
|
||||
RPC layers.
|
||||
|
||||
Subclassing Widget
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
|
||||
:js:func:`~openerp.base.Class.extend` method), and provides a number of
|
||||
abstract properties and concrete methods (which you may or may not want to
|
||||
override). Creating a subclass looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var MyWidget = openerp.base.Widget.extend({
|
||||
// QWeb template to use when rendering the object
|
||||
template: "MyQWebTemplate",
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
// insert code to execute before rendering, for object
|
||||
// initialization
|
||||
},
|
||||
start: function() {
|
||||
this._super();
|
||||
// post-rendering initialization code, at this point
|
||||
// ``this.$element`` has been initialized
|
||||
this.$element.find(".my_button").click(/* an example of event binding * /);
|
||||
|
||||
// if ``start`` is asynchronous, return a promise object so callers
|
||||
// know when the object is done initializing
|
||||
return this.rpc(/* … */)
|
||||
}
|
||||
});
|
||||
|
||||
The new class can then be used in the following manner:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Create the instance
|
||||
var my_widget = new MyWidget(this);
|
||||
// Render and insert into DOM
|
||||
my_widget.appendTo(".some-div");
|
||||
|
||||
After these two lines have executed (and any promise returned by ``appendTo``
|
||||
has been resolved if needed), the widget is ready to be used.
|
||||
|
||||
.. note:: the insertion methods will start the widget themselves, and will
|
||||
return the result of :js:func:`~openerp.base.Widget.start()`.
|
||||
|
||||
If for some reason you do not want to call these methods, you will
|
||||
have to first call :js:func:`~openerp.base.Widget.render()` on the
|
||||
widget, then insert it into your DOM and start it.
|
||||
|
||||
If the widget is not needed anymore (because it's transient), simply terminate
|
||||
it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
my_widget.stop();
|
||||
|
||||
will unbind all DOM events, remove the widget's content from the DOM and
|
||||
destroy all widget data.
|
||||
|
||||
Views
|
||||
+++++
|
||||
|
||||
@@ -541,18 +444,6 @@ Python
|
||||
.. _promise object:
|
||||
http://api.jquery.com/deferred.promise/
|
||||
|
||||
.. _.appendTo():
|
||||
http://api.jquery.com/appendTo/
|
||||
|
||||
.. _.prependTo():
|
||||
http://api.jquery.com/prependTo/
|
||||
|
||||
.. _.insertAfter():
|
||||
http://api.jquery.com/insertAfter/
|
||||
|
||||
.. _.insertBefore():
|
||||
http://api.jquery.com/insertBefore/
|
||||
|
||||
.. _Rosetta:
|
||||
.. _Launchpad's own translation tool:
|
||||
https://help.launchpad.net/Translations
|
||||
|
||||
@@ -16,6 +16,7 @@ Contents:
|
||||
async
|
||||
rpc
|
||||
|
||||
widget
|
||||
search-view
|
||||
|
||||
list-view
|
||||
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
User Interaction: Widget
|
||||
========================
|
||||
|
||||
This is the base class for all visual components. It corresponds to an MVC
|
||||
view. It provides a number of services to handle a section of a page:
|
||||
|
||||
* Rendering with QWeb
|
||||
|
||||
* Parenting-child relations
|
||||
|
||||
* Life-cycle management (including facilitating children destruction when a
|
||||
parent object is removed)
|
||||
|
||||
* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
|
||||
be anything the corresponding jQuery method accepts (generally selectors,
|
||||
DOM nodes and jQuery objects):
|
||||
|
||||
:js:func:`~openerp.base.Widget.appendTo`
|
||||
Renders the widget and inserts it as the last child of the target, uses
|
||||
`.appendTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.prependTo`
|
||||
Renders the widget and inserts it as the first child of the target, uses
|
||||
`.prependTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertAfter`
|
||||
Renders the widget and inserts it as the preceding sibling of the target,
|
||||
uses `.insertAfter()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertBefore`
|
||||
Renders the widget and inserts it as the following sibling of the target,
|
||||
uses `.insertBefore()`_
|
||||
|
||||
* Backbone-compatible shortcuts
|
||||
|
||||
DOM Root
|
||||
--------
|
||||
|
||||
A :js:class:`~openerp.web.Widget` is responsible for a section of the
|
||||
page materialized by the DOM root of the widget. The DOM root is
|
||||
available via the :js:attr:`~openerp.web.Widget.el` and
|
||||
:js:attr:`~openerp.web.Widget.$element` attributes, which are
|
||||
respectively the raw DOM Element and the jQuery wrapper around the DOM
|
||||
element.
|
||||
|
||||
There are two main ways to define and generate this DOM root:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.template
|
||||
|
||||
Should be set to the name of a QWeb template (a
|
||||
:js:class:`String`). If set, the template will be rendered after
|
||||
the widget has been initialized but before it has been
|
||||
started. The root element generated by the template will be set as
|
||||
the DOM root of the widget.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.tagName
|
||||
|
||||
Used if the widget has no template defined. Defaults to ``div``,
|
||||
will be used as the tag name to create the DOM element to set as
|
||||
the widget's DOM root. It is possible to further customize this
|
||||
generated DOM root with the following attributes:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.id
|
||||
|
||||
Used to generate an ``id`` attribute on the generated DOM
|
||||
root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.className
|
||||
|
||||
Used to generate a ``class`` attribute on the generated DOM root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.attributes
|
||||
|
||||
Mapping (object literal) of attribute names to attribute
|
||||
values. Each of these k:v pairs will be set as a DOM attribute
|
||||
on the generated DOM root.
|
||||
|
||||
None of these is used in case a template is specified on the widget.
|
||||
|
||||
The DOM root can also be defined programmatically by overridding
|
||||
|
||||
.. js:function:: openerp.web.Widget.renderElement
|
||||
|
||||
Renders the widget's DOM root and sets it. The default
|
||||
implementation will render a set template or generate an element
|
||||
as described above, and will call
|
||||
:js:func:`~openerp.web.Widget.setElement` on the result.
|
||||
|
||||
Any override to :js:func:`~openerp.web.Widget.renderElement` which
|
||||
does not call its ``_super`` **must** call
|
||||
:js:func:`~openerp.web.Widget.setElement` with whatever it
|
||||
generated or the widget's behavior is undefined.r
|
||||
|
||||
.. note::
|
||||
|
||||
The default :js:func:`~openerp.web.Widget.renderElement` can
|
||||
be called repeatedly, it will *replace* the previous DOM root
|
||||
(using ``replaceWith``). However, this requires that the
|
||||
widget correctly sets and unsets its events (and children
|
||||
widgets). Generally,
|
||||
:js:func:`~openerp.web.Widget.renderElement` should not be
|
||||
called repeatedly unless the widget advertizes this feature.
|
||||
|
||||
Accessing DOM content
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Because a widget is only responsible for the content below its DOM
|
||||
root, there is a shortcut for selecting sub-sections of a widget's
|
||||
DOM:
|
||||
|
||||
.. js:function:: openerp.web.Widget.$(selector)
|
||||
|
||||
Applies the CSS selector specified as parameter to the widget's
|
||||
DOM root.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$(selector);
|
||||
|
||||
is functionally identical to:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$element.find(selector);
|
||||
|
||||
:param String selector: CSS selector
|
||||
:returns: jQuery object
|
||||
|
||||
.. note:: this helper method is compatible with
|
||||
``Backbone.View.$``
|
||||
|
||||
Resetting the DOM root
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. js:function:: openerp.web.Widget.setElement(element)
|
||||
|
||||
Re-sets the widget's DOM root to the provided element, also
|
||||
handles re-setting the various aliases of the DOM root as well as
|
||||
unsetting and re-setting delegated events.
|
||||
|
||||
:param Element element: a DOM element or jQuery object to set as
|
||||
the widget's DOM root
|
||||
|
||||
.. note:: should be mostly compatible with `Backbone's
|
||||
setElement`_
|
||||
|
||||
DOM events handling
|
||||
-------------------
|
||||
|
||||
A widget will generally need to respond to user action within its
|
||||
section of the page. This entails binding events to DOM elements.
|
||||
|
||||
To this end, :js:class:`~openerp.web.Widget` provides an shortcut:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.events
|
||||
|
||||
Events are a mapping of ``event selector`` (an event name and a
|
||||
CSS selector separated by a space) to a callback. The callback can
|
||||
be either a method name in the widget or a function. In either
|
||||
case, the ``this`` will be set to the widget.
|
||||
|
||||
The selector is used for jQuery's `event delegation`_, the
|
||||
callback will only be triggered for descendants of the DOM root
|
||||
matching the selector [0]_. If the selector is left out (only an
|
||||
event name is specified), the event will be set directly on the
|
||||
widget's DOM root.
|
||||
|
||||
.. js:function:: openerp.web.Widget.delegateEvents
|
||||
|
||||
This method is in charge of binding
|
||||
:js:attr:`~openerp.web.Widget.events` to the DOM. It is
|
||||
automatically called after setting the widget's DOM root.
|
||||
|
||||
It can be overridden to set up more complex events than the
|
||||
:js:attr:`~openerp.web.Widget.events` map allows, but the parent
|
||||
should always be called (or :js:attr:`~openerp.web.Widget.events`
|
||||
won't be handled correctly).
|
||||
|
||||
.. js:function:: openerp.web.Widget.undelegateEvents
|
||||
|
||||
This method is in charge of unbinding
|
||||
:js:attr:`~openerp.web.Widget.events` from the DOM root when the
|
||||
widget is destroyed or the DOM root is reset, in order to avoid
|
||||
leaving "phantom" events.
|
||||
|
||||
It should be overridden to un-set any event set in an override of
|
||||
:js:func:`~openerp.web.Widget.delegateEvents`.
|
||||
|
||||
.. note:: this behavior should be compatible with `Backbone's
|
||||
delegateEvents`_, apart from not accepting any argument.
|
||||
|
||||
Subclassing Widget
|
||||
------------------
|
||||
|
||||
:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
|
||||
:js:func:`~openerp.base.Class.extend` method), and provides a number of
|
||||
abstract properties and concrete methods (which you may or may not want to
|
||||
override). Creating a subclass looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var MyWidget = openerp.base.Widget.extend({
|
||||
// QWeb template to use when rendering the object
|
||||
template: "MyQWebTemplate",
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
// insert code to execute before rendering, for object
|
||||
// initialization
|
||||
},
|
||||
start: function() {
|
||||
this._super();
|
||||
// post-rendering initialization code, at this point
|
||||
// ``this.$element`` has been initialized
|
||||
this.$element.find(".my_button").click(/* an example of event binding * /);
|
||||
|
||||
// if ``start`` is asynchronous, return a promise object so callers
|
||||
// know when the object is done initializing
|
||||
return this.rpc(/* … */)
|
||||
}
|
||||
});
|
||||
|
||||
The new class can then be used in the following manner:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Create the instance
|
||||
var my_widget = new MyWidget(this);
|
||||
// Render and insert into DOM
|
||||
my_widget.appendTo(".some-div");
|
||||
|
||||
After these two lines have executed (and any promise returned by ``appendTo``
|
||||
has been resolved if needed), the widget is ready to be used.
|
||||
|
||||
.. note:: the insertion methods will start the widget themselves, and will
|
||||
return the result of :js:func:`~openerp.base.Widget.start()`.
|
||||
|
||||
If for some reason you do not want to call these methods, you will
|
||||
have to first call :js:func:`~openerp.base.Widget.render()` on the
|
||||
widget, then insert it into your DOM and start it.
|
||||
|
||||
If the widget is not needed anymore (because it's transient), simply terminate
|
||||
it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
my_widget.destroy();
|
||||
|
||||
will unbind all DOM events, remove the widget's content from the DOM and
|
||||
destroy all widget data.
|
||||
|
||||
.. [0] not all DOM events are compatible with events delegation
|
||||
|
||||
.. _.appendTo():
|
||||
http://api.jquery.com/appendTo/
|
||||
|
||||
.. _.prependTo():
|
||||
http://api.jquery.com/prependTo/
|
||||
|
||||
.. _.insertAfter():
|
||||
http://api.jquery.com/insertAfter/
|
||||
|
||||
.. _.insertBefore():
|
||||
http://api.jquery.com/insertBefore/
|
||||
|
||||
.. _event delegation:
|
||||
http://api.jquery.com/delegate/
|
||||
|
||||
.. _Backbone's setElement:
|
||||
http://backbonejs.org/#View-setElement
|
||||
|
||||
.. _Backbone's delegateEvents:
|
||||
http://backbonejs.org/#View-delegateEvents
|
||||
|
||||
Reference in New Issue
Block a user