[MERGE] Merged latest trunk.

bzr revid: vta@openerp.com-20120802155941-oomnsjzi43fedpdc
This commit is contained in:
vta vta@openerp.com 2012-08-02 17:59:41 +02:00
commit adaa03a247
35 changed files with 1023 additions and 453 deletions

View File

@ -53,7 +53,6 @@ This module provides the core of the OpenERP Web Client.
"static/src/js/view_list.js",
"static/src/js/view_list_editable.js",
"static/src/js/view_tree.js",
"static/src/js/view_editor.js"
],
'css' : [
"static/lib/jquery.ui.bootstrap/css/custom-theme/jquery-ui-1.8.16.custom.css",

View File

@ -2474,27 +2474,6 @@
.openerp .oe_trad_field.touched {
border: 1px solid green !important;
}
.openerp .oe_view_editor {
width: 100%;
border-collapse: collapse;
margin-left: -12px;
width: 100%;
background-color: white;
border-spacing: 0;
}
.openerp .oe_view_editor td {
text-align: center;
white-space: nowrap;
border: 1px solid #d8d8d8;
cursor: pointer;
font-size: 90%;
}
.openerp .oe_view_editor_field td {
border: 0px !important;
}
.openerp .oe_view_editor tr:hover {
background-color: #ecebf2;
}
.openerp .oe_layout_debugging .oe_form_group {
outline: 2px dashed green;
}

View File

@ -1929,25 +1929,6 @@ $sheet-max-width: 860px
.oe_trad_field.touched
border: 1px solid green !important
// }}}
// View Editor {{{
.oe_view_editor
width: 100%
border-collapse: collapse
margin-left: -12px
width: 100%
background-color: white
border-spacing: 0
td
text-align: center
white-space: nowrap
border: 1px solid #D8D8D8
cursor: pointer
font-size: 90%
.oe_view_editor_field td
border: 0px !important
.oe_view_editor tr:hover
background-color: #ecebf2
// }}}
// Debugging stuff {{{
.oe_layout_debugging
.oe_form_group

View File

@ -48,7 +48,7 @@
* OpenERP Web web module split
*---------------------------------------------------------*/
openerp.web = function(session) {
var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import","view_editor"];
var files = ["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);

View File

@ -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.content_to_set = content;
this.dialog_options = {
modal: true,
destroy_on_close: true,
@ -83,11 +81,6 @@ instance.web.Dialog = instance.web.Widget.extend({
if (options) {
_.extend(this.dialog_options, options);
}
if (this.dialog_options.autoOpen) {
this.open();
} else {
instance.web.dialog(this.$element, this.get_options());
}
},
get_options: function(options) {
var self = this,
@ -116,31 +109,44 @@ instance.web.Dialog = instance.web.Widget.extend({
} else if (val.slice(-1) == "%") {
return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
} else {
return parseInt(val, 10);
return parseInt(val, 10);
}
},
renderElement: function() {
if (this.content_to_set) {
this.setElement(this.content_to_set);
} else if (this.template) {
this._super();
}
},
open: function(options) {
// TODO fme: bind window on resize
if (this.template) {
this.$element.html(this.renderElement());
}
if (! this.dialog_inited)
this.init_dialog();
var o = this.get_options(options);
instance.web.dialog(this.$element, o).dialog('open');
instance.web.dialog(this.$element, o).dialog('open');
if (o.height === 'auto' && o.max_height) {
this.$element.css({ 'max-height': o.max_height, 'overflow-y': 'auto' });
}
return this;
},
init_dialog: function(options) {
this.renderElement();
var o = this.get_options(options);
instance.web.dialog(this.$element, o);
var res = this.start();
this.dialog_inited = true;
return res;
},
close: function() {
this.$element.dialog('close');
},
on_close: function() {
if (this.__tmp_dialog_destroying)
return;
if (this.__tmp_dialog_destroying)
return;
if (this.dialog_options.destroy_on_close) {
this.__tmp_dialog_closing = true;
this.__tmp_dialog_closing = true;
this.destroy();
this.__tmp_dialog_closing = undefined;
this.__tmp_dialog_closing = undefined;
}
},
on_resized: function() {
@ -150,10 +156,10 @@ instance.web.Dialog = instance.web.Widget.extend({
el.destroy();
});
if (! this.__tmp_dialog_closing) {
this.__tmp_dialog_destroying = true;
this.close();
this.__tmp_dialog_destroying = undefined;
}
this.__tmp_dialog_destroying = true;
this.close();
this.__tmp_dialog_destroying = undefined;
}
if (! this.isDestroyed()) {
this.$element.dialog('destroy');
}
@ -263,11 +269,11 @@ instance.web.Loading = instance.web.Widget.extend({
this.count += increment;
if (this.count > 0) {
if (instance.connection.debug) {
this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count));
} else {
this.$element.text(_t("Loading"));
}
if (instance.connection.debug) {
this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count));
} else {
this.$element.text(_t("Loading"));
}
this.$element.show();
this.getParent().$element.addClass('oe_wait');
} else {
@ -496,7 +502,11 @@ instance.web.Login = instance.web.Widget.extend({
this.has_local_storage = typeof(localStorage) != 'undefined';
this.selected_db = null;
this.selected_login = null;
this.params = params;
this.params = params || {};
if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful);
}
if (this.has_local_storage && this.remember_credentials) {
this.selected_db = localStorage.getItem('last_db_login_success');
@ -513,7 +523,7 @@ instance.web.Login = instance.web.Widget.extend({
self.do_action("database_manager");
});
return self.load_db_list().then(self.on_db_list_loaded).then(function() {
if(self.params) {
if (self.params.db) {
self.do_login(self.params.db, self.params.login, self.params.password);
}
});
@ -589,6 +599,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 +896,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() {
@ -914,8 +929,10 @@ instance.web.Client = instance.web.Widget.extend({
}
}, 0);
});
instance.web.bus.on('click', this, function() {
self.$element.find('.oe_dropdown_menu.oe_opened').removeClass('oe_opened');
instance.web.bus.on('click', this, function(ev) {
if (!$(ev.target).is('input[type=file]')) {
self.$element.find('.oe_dropdown_menu.oe_opened').removeClass('oe_opened');
}
});
},
show_common: function() {

View File

@ -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
@ -1039,7 +1135,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
uid: new py.float(this.uid),
datetime: datetime,
time: time,
relativedelta: relativedelta
relativedelta: relativedelta,
current_date: date.today.__call__().strftime(['%Y-%m-%d'])
};
},
/**
@ -1280,7 +1377,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
processData: false
}, url);
if (this.synch)
ajax.async = false;
ajax.async = false;
return $.ajax(ajax);
},
rpc_jsonp: function(url, payload) {
@ -1299,7 +1396,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
data: data
}, url);
if (this.synch)
ajax.async = false;
ajax.async = false;
var payload_str = JSON.stringify(payload);
var payload_url = $.param({r:payload_str});
if(payload_url.length < 2000) {

View File

@ -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;
},
@ -394,13 +393,13 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
timer = setTimeout(waitLoop, CHECK_INTERVAL);
},
synchronized_mode: function(to_execute) {
var synch = this.synch;
this.synch = true;
try {
return to_execute();
} finally {
this.synch = synch;
}
var synch = this.synch;
this.synch = true;
try {
return to_execute();
} finally {
this.synch = synch;
}
}
});
@ -416,12 +415,12 @@ instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin,
// check gtk bindings
// http://unixpapa.com/js/key.html
_.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
$('html').on(evtype, self, function(ev) {
$('html').on(evtype, function(ev) {
self.trigger(evtype, ev);
});
});
_.each('resize,scroll'.split(','), function(evtype) {
$(window).on(evtype, self, function(ev) {
$(window).on(evtype, function(ev) {
self.trigger(evtype, ev);
});
});
@ -541,10 +540,10 @@ $.async_when = function() {
// special tweak for the web client
var old_async_when = $.async_when;
$.async_when = function() {
if (instance.connection.synch)
return $.when.apply(this, arguments);
else
return old_async_when.apply(this, arguments);
if (instance.connection.synch)
return $.when.apply(this, arguments);
else
return old_async_when.apply(this, arguments);
};
/** Setup blockui */

View File

@ -803,7 +803,7 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da
return this.ids.length;
},
alter_ids: function(n_ids) {
this.ids = n_ids;
this.ids = n_ids;
},
});
instance.web.DataSetStatic = instance.web.DataSet.extend({
@ -1042,7 +1042,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
self.cache.push({id: id, values: record});
} else {
// I assume cache value is prioritary
cached.values = _.defaults(_.clone(cached.values), record);
cached.values = _.defaults(_.clone(cached.values), record);
}
});
return_records();
@ -1067,7 +1067,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
return this._super(method, args, callback, error_callback);
},
alter_ids: function(n_ids) {
this._super(n_ids);
this._super(n_ids);
this.on_change();
},
});

View File

@ -156,7 +156,7 @@ instance.web.DataImport = instance.web.Dialog.extend({
});
},
toggle_import_button: function (newstate) {
instance.web.dialog(this.$element, 'widget')
instance.web.dialog(this.$element, 'widget')
.find('.oe_import_dialog_button')
.button('option', 'disabled', !newstate);
},

View File

@ -661,7 +661,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
null, _(this.select_for_drawer()).invoke(
'appendTo', this.$element.find('.oe_searchview_drawer')));
new instance.web.search.AddToDashboard(this).appendTo($('.oe_searchview_drawer', this.$element));
new instance.web.search.AddToReporting(this).appendTo($('.oe_searchview_drawer', this.$element));
// load defaults
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
@ -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
*/
@ -1678,42 +1678,39 @@ instance.web.search.Filters = instance.web.search.Input.extend({
}));
}
});
instance.web.search.AddToDashboard = instance.web.Widget.extend({
template: 'SearchView.addtodashboard',
instance.web.search.AddToReporting = instance.web.Widget.extend({
template: 'SearchView.addtoreporting',
_in_drawer: true,
start: function () {
var self = this;
this.data_loaded = $.Deferred();
this.dashboard_data =[];
this.$element
.on('click', 'h4', this.proxy('show_option'))
.on('submit', 'form', function (e) {
e.preventDefault();
self.add_dashboard();
});
return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data"));
return this.load_data().then(this.proxy("render_data"));
},
load_data:function(){
if (!instance.webclient) { return $.Deferred().reject(); }
var self = this,dashboard_menu = instance.webclient.menu.data.data.children;
var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']);
var map_data = function(result){
_.detect(dashboard_menu, function(dash){
var id = _.pluck(dash.children, "id"),indexof = _.indexOf(id, result.res_id);
if(indexof !== -1){
self.dashboard_data = dash.children[indexof].children
self.data_loaded.resolve();
return;
}
});
};
return ir_model_data._execute().done(function(result){map_data(result[0])});
var dashboard_menu = instance.webclient.menu.data.data.children;
return new instance.web.Model('ir.model.data')
.query(['res_id'])
.filter([['name','=','menu_reporting_dashboard']])
.first().pipe(function (result) {
var menu = _(dashboard_menu).chain()
.pluck('children')
.flatten(true)
.find(function (child) { return child.id === result.res_id; })
.value();
return menu ? menu.children : [];
});
},
render_data: function(){
var self = this;
var selection = instance.web.qweb.render("SearchView.addtodashboard.selection",{selections:this.dashboard_data});
this.$element.find("input").before(selection)
render_data: function(dashboard_choices){
var selection = instance.web.qweb.render(
"SearchView.addtoreporting.selection", {
selections: dashboard_choices});
this.$("input").before(selection)
},
add_dashboard:function(){
var self = this;
@ -1721,11 +1718,11 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({
var view_parent = this.getParent().getParent();
if (! view_parent.action || ! this.$element.find("select").val())
return this.do_warn("Can't find dashboard action");
data = getParent.build_search_data(),
context = new instance.web.CompoundContext(getParent.dataset.get_context() || []),
domain = new instance.web.CompoundDomain(getParent.dataset.get_domain() || []);
_.each(data.contexts, function(x) {context.add(x);});
_.each(data.domains, function(x) {domain.add(x);});
var data = getParent.build_search_data();
var context = new instance.web.CompoundContext(getParent.dataset.get_context() || []);
var domain = new instance.web.CompoundDomain(getParent.dataset.get_domain() || []);
_.each(data.contexts, context.add, context);
_.each(data.domains, domain.add, domain);
this.rpc('/web/searchview/add_to_dashboard', {
menu_id: this.$element.find("select").val(),
action_id: view_parent.action.id,
@ -1746,7 +1743,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({
this.$element.toggleClass('oe_opened');
if (! this.$element.hasClass('oe_opened'))
return;
this.$element.find("input").val(this.getParent().fields_view.name || "" );
this.$("input").val(this.getParent().fields_view.name || "" );
}
});

View File

@ -79,6 +79,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
_.defaults(this.options, {
"not_interactible_on_create": false,
"initial_mode": "view",
"disable_autofocus": false,
});
this.is_initialized = $.Deferred();
this.mutating_mutex = new $.Mutex();
@ -101,7 +102,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) {
@ -267,6 +270,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (this.$pager) {
this.$pager.show();
}
this.$element.show().css({
opacity: '0',
filter: 'alpha(opacity = 0)'
});
this.$element.add(this.$buttons).removeClass('oe_form_dirty');
var shown = this.has_been_loaded;
@ -287,6 +294,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (options.editable) {
self.to_edit_mode();
}
self.$element.css({
opacity: '1',
filter: 'alpha(opacity = 100)'
});
});
},
do_hide: function () {
@ -346,6 +357,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
self.do_push_state({id:record.id});
}
self.$element.add(self.$buttons).removeClass('oe_form_dirty');
self.autofocus();
});
},
/**
@ -646,15 +658,21 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
_.each(this.fields,function(field){
field.set({"force_readonly": false});
});
var fields_order = self.fields_order.slice(0);
if (self.default_focus_field) {
fields_order.unshift(self.default_focus_field.name);
this.autofocus();
}
},
autofocus: function() {
if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
var fields_order = this.fields_order.slice(0);
if (this.default_focus_field) {
fields_order.unshift(this.default_focus_field.name);
}
for (var i = 0; i < fields_order.length; i += 1) {
var field = self.fields[fields_order[i]];
var field = this.fields[fields_order[i]];
if (!field.get('effective_invisible') && !field.get('effective_readonly')) {
field.focus();
break;
if (field.focus() !== false) {
break;
}
}
}
}
@ -1961,14 +1979,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
}
},
focus: function() {
},
/**
* Utility method to focus an element, but only after a small amount of time.
*/
delay_focus: function($elem) {
setTimeout(function() {
$elem[0].focus();
}, 50);
return false;
},
/**
* Utility method to get the widget options defined in the field xml description.
@ -2067,7 +2078,7 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function() {
this.$element.find('input:first')[0].focus();
this.$element.find('input:first').focus();
}
});
@ -2287,8 +2298,9 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc
return this.get('value') === '' || this._super();
},
focus: function() {
if (this.datewidget && this.datewidget.$input)
this.delay_focus(this.datewidget.$input);
if (this.datewidget && this.datewidget.$input) {
this.datewidget.$input.focus();
}
}
});
@ -2343,7 +2355,7 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function($element) {
this.delay_focus(this.$textarea);
this.$textarea.focus();
},
do_resize: function(max_height) {
max_height = parseInt(max_height, 10);
@ -2436,7 +2448,7 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
this.$checkbox[0].checked = value_;
},
focus: function() {
this.delay_focus(this.$checkbox);
this.$checkbox.focus();
}
});
@ -2460,9 +2472,6 @@ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
}
});
instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({
// to replace view editor
});
instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: 'FieldSelection',
@ -2531,7 +2540,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan
return !! value_;
},
focus: function() {
this.delay_focus(this.$element.find('select:first'));
this.$element.find('select:first').focus();
}
});
@ -2818,6 +2827,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
} else if (item.action) {
self.floating = true;
item.action();
// Cancel widget blurring, to avoid form blur event
self.trigger('focused');
return false;
}
},
@ -2921,7 +2932,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
return ! this.get("value");
},
focus: function () {
this.delay_focus(this.$input);
this.$input.focus();
}
});
@ -3077,7 +3088,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();
@ -3491,10 +3501,23 @@ instance.web.form.One2ManyList = instance.web.ListView.List.extend({
colspan: columns,
'class': 'oe_form_field_one2many_list_row_add'
}).text(_t("Add a row"))
.mousedown(function () {
// FIXME: needs to be an official API somehow
if (self.view.editor.is_editing()) {
self.view.__ignore_blur = true;
}
})
.click(function (e) {
e.preventDefault();
e.stopPropagation();
self.view.do_add_record();
// FIXME: there should also be an API for that one
if (self.view.editor.form.__blur_timeout) {
clearTimeout(self.view.editor.form.__blur_timeout);
self.view.editor.form.__blur_timeout = false;
}
self.view.ensure_saved().then(function () {
self.view.do_add_record();
});
});
this.$current.append(
$('<tr>').append($cell))
@ -4305,7 +4328,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
@ -4318,7 +4341,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
@ -4698,9 +4721,6 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
elem.css("color", color);
}
},
focus: function() {
return false;
},
});
/**

View File

@ -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();
@ -604,6 +602,7 @@ openerp.web.list_editable = function (instance) {
this.form = new (this.options.formView)(
this, this.delegate.dataset, false, {
initial_mode: 'edit',
disable_autofocus: true,
$buttons: $(),
$pager: $()
});
@ -683,9 +682,8 @@ openerp.web.list_editable = function (instance) {
if (!field.$element.is(':visible')) {
return false;
}
field.focus();
// Stop as soon as a field got focused
return true;
return field.focus() !== false;
});
},
edit: function (record, configureField, options) {

View File

@ -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) {
@ -435,7 +435,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
container.hide();
controller.do_hide();
}
// put the <footer> in the dialog's buttonpane
// put the <footer> in the dialog's buttonpane
if (self.$element.parent('.ui-dialog-content') && self.$element.find('footer')) {
self.$element.parent('.ui-dialog-content').parent().find('div.ui-dialog-buttonset').hide()
self.$element.find('footer').appendTo(
@ -445,7 +445,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
}
});
});
return view_promise;
},
do_create_view: function(view_type) {
// Lazy loading of views
@ -529,7 +528,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
}
return controller.get('title');
});
if (next && next.action.res_id && self.active_view === 'form' && self.model === next.action.res_model && id === next.action.res_id) {
if (next && next.action && next.action.res_id && self.active_view === 'form' && self.model === next.action.res_model && id === next.action.res_id) {
// If the current active view is a formview and the next item in the breadcrumbs
// is an action on same object (model / res_id), then we omit the current formview's title
titles.pop();
@ -756,15 +755,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
width: '95%'}, $root).open();
});
break;
case 'manage_views':
if (current_view.fields_view && current_view.fields_view.arch) {
var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch);
view_editor.start();
} else {
this.do_warn(_t("Manage Views"),
_t("Could not find current view declaration"));
}
break;
case 'edit_workflow':
return this.do_action({
res_model : 'workflow',
@ -1175,7 +1165,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() {
@ -1385,7 +1375,6 @@ instance.web.xml_to_json = function(node) {
}
instance.web.json_node_to_xml = function(node, human_readable, indent) {
// For debugging purpose, this function will convert a json node back to xml
// Maybe useful for xml view editor
indent = indent || 0;
var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
r = sindent + '<' + node.tag,

View File

@ -406,10 +406,10 @@
<t t-name="ViewManager">
<div class="oe_view_manager">
<table class="oe_view_manager_header">
<col width="20%"/>
<col width="25%"/>
<col width="20%"/>
<col width="35%"/>
<col width="20%"/>
<col width="25%"/>
<col width="20%"/>
<col width="35%"/>
<tr class="oe_header_row oe_header_row_top">
<td colspan="2">
<h2 class="oe_view_title" t-if="widget.flags.display_title !== false">
@ -1443,18 +1443,18 @@
<div>
</div>
</div>
<div t-name="SearchView.addtodashboard" class="oe_searchview_dashboard">
<h4>Add to Dashboard</h4>
<div t-name="SearchView.addtoreporting" class="oe_searchview_dashboard">
<h4>Add to Reporting</h4>
<form>
<p><input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/></p>
<button class="oe_apply" type="submit">Save</button>
<p><input placeholder="Title of new dashboard item"/></p>
<button class="oe_apply" type="submit">Add</button>
</form>
</div>
<t t-name="SearchView.addtodashboard.selection">
<select title = "Select Dashboard to add this filter to">
<t t-foreach="selections" t-as="element">
<option t-att-value="element.id || element.res_id "><t t-esc="element.name"/></option>
</t>
<t t-name="SearchView.addtoreporting.selection">
<select>
<option t-foreach="selections" t-as="element"
t-att-value="element.id || element.res_id ">
<t t-esc="element.name"/></option>
</select>
</t>
<div t-name="SearchView.advanced" class="oe_searchview_advanced">
@ -1502,70 +1502,6 @@
</select>
</t>
<t t-name="view_editor">
<table class="oe_view_editor">
<t t-call="view_editor.row"/>
</table>
</t>
<t t-name="view_editor.row">
<tr t-att-id="'viewedit-' + rec.id" t-att-level="rec.level" t-foreach="data" t-as="rec">
<td width="90%">
<table class="oe_view_editor_field">
<tr>
<td width="16px" t-att-style="'background-position: ' + 20*rec.level + 'px; padding-left: ' + 20*rec.level + 'px'">
<img t-if="rec.child_id.length" t-att-id="'parentimg-' + rec.id"
t-att-src='_s + "/web/static/src/img/collapse.gif"' width="16" height="16" border="0"/>
</td>
<td style="cursor: pointer;">
<a style="text-decoration:none" href="javascript:void(0);">
<t t-esc="rec.name"/>
</a>
</td>
</tr>
</table>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-add" t-att-src='_s + "/web/static/src/img/icons/gtk-add.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img id="side-remove" t-att-src='_s + "/web/static/src/img/icons/gtk-remove.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length and !_.include(no_properties, rec.att_list[0])"
id="side-edit" t-att-src='_s + "/web/static/src/img/icons/gtk-edit.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-up" t-att-src='_s + "/web/static/src/img/icons/gtk-go-up.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-down" t-att-src='_s + "/web/static/src/img/icons/gtk-go-down.png"' style="cursor: pointer;"/>
</td>
<t t-if="rec.child_id.length">
<t t-set="data" t-value="rec.child_id"/>
<t t-call="view_editor.row"/>
</t>
</tr>
</t>
<t t-name="vieweditor_char">
<input type="text" t-att-id="widget.name" class="field_char" size="50"/>
</t>
<t t-name="vieweditor_selection">
<select t-att-id="widget.name" >
<t t-if="widget.selection" t-foreach="widget.selection" t-as="option">
<option
t-att-value="typeof option === 'object' ? option[0] : option">
<t t-esc="typeof option === 'object' ? option[1] : option"/>
</option>
</t>
</select>
</t>
<t t-name="vieweditor_boolean">
<input type="checkbox" t-att-id="widget.name"/>
</t>
<t t-name="ExportView">
<a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a>
</t>

View File

@ -0,0 +1,239 @@
$(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({ }));
var $original = w.$element;
ok($original, "should initially have a root element");
w.renderElement();
ok(w.$element, "should have generated a root element");
ok($original !== w.$element, "should have generated a new 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");
});
});
});

View File

@ -133,4 +133,20 @@ $(document).ready(function () {
}]);
deepEqual(result, {type: 'out_invoice'});
});
module('eval.domains', {
setup: function () {
openerp = window.openerp.testing.instanceFor('coresetup');
window.openerp.web.dates(openerp);
}
});
test('current_date', function () {
var current_date = openerp.web.date_to_str(new Date());
var result = openerp.connection.test_eval_domains(
[[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
openerp.connection.test_eval_get_context());
deepEqual(result, [
['name', '>=', current_date],
['name', '<=', current_date]
]);
})
});

View File

@ -72,7 +72,7 @@ $(document).ready(function () {
});
asyncTest('base-state', 2, function () {
var e = new instance.web.list.Editor({
dataset: {},
dataset: {ids: []},
edition_view: function () {
return makeFormView();
}
@ -240,8 +240,7 @@ $(document).ready(function () {
};
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
var l = new instance.web.ListView({}, ds);
l.set_editable(true);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.appendTo($fix)
.pipe(l.proxy('reload_content'))
@ -305,8 +304,7 @@ $(document).ready(function () {
counter: 0,
onEvent: function (e) { this.counter++; }
};
var l = new instance.web.ListView({}, ds);
l.set_editable(true);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.on('edit:before edit:after', o, o.onEvent);
l.appendTo($fix)
.pipe(l.proxy('reload_content'))
@ -326,8 +324,7 @@ $(document).ready(function () {
asyncTest('edition events: cancelling', 3, function () {
var edit_after = false;
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
var l = new instance.web.ListView({}, ds);
l.set_editable(true);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.on('edit:before', {}, function (e) {
e.cancel = true;
});

View File

@ -42,7 +42,7 @@
<script src="/web/static/test/testing.js"></script>
<script type="text/javascript">
QUnit.config.testTimeout = 500;
QUnit.config.testTimeout = 2000;
</script>
</head>
<body id="oe" class="openerp">
@ -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>

View File

@ -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());
});

View File

@ -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;

View File

@ -40,6 +40,9 @@
.openerp .oe_kanban_view .oe_kanban_groups {
height: inherit;
}
.openerp .oe_kanban_view.oe_kanban_ungrouped .oe_kanban_groups {
width: 100%;
}
.openerp .oe_kanban_view .oe_kanban_header:hover .oe_dropdown_kanban {
display: inline-block;
}
@ -65,7 +68,7 @@
.openerp .oe_kanban_view .oe_kanban_group_header.oe_kanban_no_group {
padding: 0px;
}
.openerp .oe_kanban_view .oe_kanban_column.oe_kanban_grouped, .openerp .oe_kanban_view .oe_kanban_group_header {
.openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_column, .openerp .oe_kanban_view .oe_kanban_group_header {
background: #f0eeee;
border-left: 1px solid #f0f8f8;
border-right: 1px solid #b9b9b9;
@ -185,7 +188,7 @@
font-weight: bold;
margin: 2px 4px;
}
.openerp .oe_kanban_view .oe_kanban_grouped .oe_kanban_record {
.openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_record {
margin-bottom: 6px;
}
.openerp .oe_kanban_view .oe_kanban_gravatar {
@ -236,13 +239,13 @@
clear: both;
text-align: center;
}
.openerp .oe_kanban_view .oe_kanban_grouped .oe_kanban_show_more .oe_button {
.openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_show_more .oe_button {
width: 100%;
}
.openerp .oe_kanban_view .oe_kanban_ungrouped {
.openerp .oe_kanban_view.oe_kanban_ungrouped .oe_kanban_column {
background: white;
}
.openerp .oe_kanban_view .oe_kanban_ungrouped .oe_kanban_record {
.openerp .oe_kanban_view.oe_kanban_ungrouped .oe_kanban_column .oe_kanban_record {
float: left;
padding: 2px;
box-sizing: border-box;

View File

@ -63,6 +63,8 @@
// KanbanGroups {{{
.oe_kanban_groups
height: inherit
&.oe_kanban_ungrouped .oe_kanban_groups
width: 100%
.oe_kanban_header
&:hover
.oe_dropdown_kanban
@ -86,7 +88,7 @@
.oe_kanban_group_header.oe_kanban_no_group
padding: 0px
.oe_kanban_column.oe_kanban_grouped, .oe_kanban_group_header
&.oe_kanban_grouped .oe_kanban_column, .oe_kanban_group_header
background: #f0eeee
border-left: 1px solid #f0f8f8
border-right: 1px solid #b9b9b9
@ -187,7 +189,7 @@
.oe_kanban_title
font-weight: bold
margin: 2px 4px
.oe_kanban_grouped .oe_kanban_record
&.oe_kanban_grouped .oe_kanban_record
margin-bottom: 6px
.oe_kanban_gravatar
display: block
@ -222,9 +224,9 @@
.oe_kanban_show_more
clear: both
text-align: center
.oe_kanban_grouped .oe_kanban_show_more .oe_button
&.oe_kanban_grouped .oe_kanban_show_more .oe_button
width: 100%
.oe_kanban_ungrouped
&.oe_kanban_ungrouped .oe_kanban_column
background: white
.oe_kanban_record
float: left

View File

@ -29,8 +29,6 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
records : {}
};
this.groups = [];
this.form_dialog = new instance.web.form.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
this.form_dialog.on_form_dialog_saved.add_last(this.do_reload);
this.aggregates = {};
this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
this.qweb = new QWeb2.Engine();
@ -173,6 +171,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
},
do_process_groups: function(groups) {
var self = this;
this.$element.remove('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
this.add_group_mutex.exec(function() {
self.do_clear_groups();
self.dataset.ids = [];
@ -194,6 +193,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
},
do_process_dataset: function(dataset) {
var self = this;
this.$element.remove('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
this.add_group_mutex.exec(function() {
var def = $.Deferred();
self.do_clear_groups();
@ -702,14 +702,7 @@ instance.web_kanban.KanbanRecord = instance.web.OldWidget.extend({
return do_it();
},
do_action_edit: function($action) {
var self = this;
if ($action.attr('target') === 'dialog') {
this.view.form_dialog.select_id(this.id).then(function() {
self.view.form_dialog.open();
});
} else {
this.view.open_record(this.id, true);
}
this.view.open_record(this.id, true);
},
do_action_object: function ($action) {
var button_attrs = $action.data();
@ -720,9 +713,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;
self.replaceElement($(self.render()));
self.$element.data('widget', self);
self.bind_events();
self.group.compute_cards_auto_height();

View File

@ -55,7 +55,7 @@
</td>
</t>
<t t-name="KanbanView.group_records_container">
<td t-attf-class="oe_kanban_column #{widget.group ? 'oe_kanban_grouped' : 'oe_kanban_ungrouped'}">
<td class="oe_kanban_column">
<div class="oe_kanban_group_list_header"/>
<div class="oe_kanban_show_more">
<button class="oe_button">Show more... (<span class="oe_kanban_remaining"></span> remaining)</button>

View File

@ -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);
}

View File

@ -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,

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,14 @@
{
"name": "View Editor",
"category": "Hidden",
"description":
"""
OpenERP Web to edit views.
""",
"version": "2.0",
"depends":['web'],
"js": ["static/src/js/view_editor.js"],
"css": ['static/src/css/view_editor.css'],
"qweb": ['static/src/xml/view_editor.xml'],
'auto_install': True,
}

View File

@ -0,0 +1,19 @@
# Translations template for PROJECT.
# Copyright (C) 2012 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2012-07-30 14:37+0530\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"

View File

@ -0,0 +1,21 @@
.openerp .oe_view_editor {
width: 100%;
border-collapse: collapse;
margin-left: -12px;
width: 100%;
background-color: white;
border-spacing: 0;
}
.openerp .oe_view_editor td {
text-align: center;
white-space: nowrap;
border: 1px solid #d8d8d8;
cursor: pointer;
font-size: 90%;
}
.openerp .oe_view_editor_field td {
border: 0px !important;
}
.openerp .oe_view_editor tr:hover {
background-color: #ecebf2;
}

View File

@ -1,7 +1,25 @@
openerp.web.view_editor = function(instance) {
openerp.web_view_editor = function(instance) {
var _t = instance.web._t;
var QWeb = instance.web.qweb;
instance.web.ViewEditor = instance.web.OldWidget.extend({
instance.web.ViewManagerAction.include({
on_debug_changed:function(evt){
var val = $(evt.currentTarget).find('option:selected').val(),
current_view = this.views[this.active_view].controller;
if(val === "manage_views"){
if (current_view.fields_view && current_view.fields_view.arch) {
var view_editor = new instance.web_view_editor.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch);
view_editor.start();
} else {
this.do_warn(_t("Manage Views"),
_t("Could not find current view declaration"));
}
evt.currentTarget.selectedIndex = 0;
}else{
return this._super.apply(this,arguments);
}
}
})
instance.web_view_editor.ViewEditor = instance.web.OldWidget.extend({
init: function(parent, element_id, dataset, view, options) {
this._super(parent);
this.element_id = element_id;
@ -9,7 +27,7 @@ instance.web.ViewEditor = instance.web.OldWidget.extend({
this.dataset = new instance.web.DataSetSearch(this, 'ir.ui.view', null, null),
this.model = dataset.model;
this.xml_element_id = 0;
this.property = instance.web.ViewEditor.property_widget;
this.property = instance.web_view_editor.ViewEditor.property_widget;
this.one_object = false;
},
start: function() {
@ -350,7 +368,7 @@ instance.web.ViewEditor = instance.web.OldWidget.extend({
this.edit_xml_dialog.$element.find("tr[id=viewedit-" + row_id + "]").addClass('ui-selected');
},
do_parent_img_hide_show: function(img) {
if ($(img).attr('src') == '/web/static/src/img/collapse.gif') {
if (_.str.include($(img).attr('src'), '/web/static/src/img/collapse.gif')) {
$(img).attr('src', '/web/static/src/img/expand.gif');
this.on_expand(img);
} else {
@ -434,7 +452,7 @@ instance.web.ViewEditor = instance.web.OldWidget.extend({
break;
}
if (view_find.attr('level') < min_level) {
min_level = parseInt(view_find.attr('level'));
min_level = parseInt(view_find.attr('level'));
}
}
var val = _.detect(obj.att_list, function(val) {return val[0] == "name";});
@ -1013,7 +1031,7 @@ instance.web.ViewEditor = instance.web.OldWidget.extend({
});
}
});
instance.web.ViewEditor.Field = instance.web.Class.extend({
instance.web_view_editor.ViewEditor.Field = instance.web.Class.extend({
init: function(view, widget) {
this.$element = view.$element;
this.dirty = false;
@ -1049,7 +1067,7 @@ instance.web.ViewEditor.Field = instance.web.Class.extend({
return _.str.sprintf("<td id = %s>%s</td>", this.name, QWeb.render(this.template, {widget: this}))
}
});
instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({
instance.web_view_editor.ViewEditor.FieldBoolean = instance.web_view_editor.ViewEditor.Field.extend({
template : "vieweditor_boolean",
start: function() {
var self = this;
@ -1067,7 +1085,7 @@ instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({
return this.$element.find("input[id=" + this.name + "]").is(':checked')? "1" : null;
}
});
instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({
instance.web_view_editor.ViewEditor.FieldChar = instance.web_view_editor.ViewEditor.Field.extend({
template : "vieweditor_char",
start: function () {
var self = this;
@ -1083,7 +1101,7 @@ instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({
return this.$element.find("input[id=" + this.name + "]").val();
}
});
instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({
instance.web_view_editor.ViewEditor.FieldSelect = instance.web_view_editor.ViewEditor.Field.extend({
template : "vieweditor_selection",
start: function () {
var self = this;
@ -1113,7 +1131,7 @@ instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({
return this.$element.find("select[id=" + this.name + "]").val();
}
});
instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.extend({
instance.web_view_editor.ViewEditor.FieldSelectMulti = instance.web_view_editor.ViewEditor.FieldSelect.extend({
start: function () {
this._super();
this.$element.find("select[id=" + this.name + "]").css('height', '100px').attr("multiple", true);
@ -1129,7 +1147,7 @@ instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.e
});
}
});
instance.web.ViewEditor.FieldFloat = instance.web.ViewEditor.FieldChar.extend({
instance.web_view_editor.ViewEditor.FieldFloat = instance.web_view_editor.ViewEditor.FieldChar.extend({
});
var _PROPERTIES = {
@ -1202,11 +1220,11 @@ var _ICONS = ['','STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph'
];
instance.web.ViewEditor.property_widget = new instance.web.Registry({
'boolean' : 'instance.web.ViewEditor.FieldBoolean',
'selection_multi' : 'instance.web.ViewEditor.FieldSelectMulti',
'selection' : 'instance.web.ViewEditor.FieldSelect',
'char' : 'instance.web.ViewEditor.FieldChar',
'float' : 'instance.web.ViewEditor.FieldFloat'
instance.web_view_editor.ViewEditor.property_widget = new instance.web.Registry({
'boolean' : 'instance.web_view_editor.ViewEditor.FieldBoolean',
'selection_multi' : 'instance.web_view_editor.ViewEditor.FieldSelectMulti',
'selection' : 'instance.web_view_editor.ViewEditor.FieldSelect',
'char' : 'instance.web_view_editor.ViewEditor.FieldChar',
'float' : 'instance.web_view_editor.ViewEditor.FieldFloat'
});
};

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-name="view_editor">
<table class="oe_view_editor">
<t t-call="view_editor.row"/>
</table>
</t>
<t t-name="view_editor.row">
<tr t-att-id="'viewedit-' + rec.id" t-att-level="rec.level" t-foreach="data" t-as="rec">
<td width="90%">
<table class="oe_view_editor_field">
<tr>
<td width="16px" t-att-style="'background-position: ' + 20*rec.level + 'px; padding-left: ' + 20*rec.level + 'px'">
<img t-if="rec.child_id.length" t-att-id="'parentimg-' + rec.id"
t-att-src='_s + "/web/static/src/img/collapse.gif"' width="16" height="16" border="0"/>
</td>
<td style="cursor: pointer;">
<a style="text-decoration:none" href="javascript:void(0);">
<t t-esc="rec.name"/>
</a>
</td>
</tr>
</table>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-add" t-att-src='_s + "/web/static/src/img/icons/gtk-add.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img id="side-remove" t-att-src='_s + "/web/static/src/img/icons/gtk-remove.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length and !_.include(no_properties, rec.att_list[0])"
id="side-edit" t-att-src='_s + "/web/static/src/img/icons/gtk-edit.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-up" t-att-src='_s + "/web/static/src/img/icons/gtk-go-up.png"' style="cursor: pointer;"/>
</td>
<td width="2%">
<img t-if="rec.att_list.length"
id="side-down" t-att-src='_s + "/web/static/src/img/icons/gtk-go-down.png"' style="cursor: pointer;"/>
</td>
<t t-if="rec.child_id.length">
<t t-set="data" t-value="rec.child_id"/>
<t t-call="view_editor.row"/>
</t>
</tr>
</t>
<t t-name="vieweditor_char">
<input type="text" t-att-id="widget.name" class="field_char" size="50"/>
</t>
<t t-name="vieweditor_selection">
<select t-att-id="widget.name" >
<t t-if="widget.selection" t-foreach="widget.selection" t-as="option">
<option
t-att-value="typeof option === 'object' ? option[0] : option">
<t t-esc="typeof option === 'object' ? option[1] : option"/>
</option>
</t>
</select>
</t>
<t t-name="vieweditor_boolean">
<input type="checkbox" t-att-id="widget.name"/>
</t>
</template>

View File

@ -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

View File

@ -16,6 +16,7 @@ Contents:
async
rpc
widget
search-view
list-view

274
doc/widget.rst Normal file
View File

@ -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