[MERGE] trunk

bzr revid: al@openerp.com-20120801142851-kmc1obvwu6kdwdin
This commit is contained in:
Antony Lesuisse 2012-08-01 16:28:51 +02:00
commit 2948fca067
40 changed files with 1441 additions and 785 deletions

View File

@ -3,8 +3,10 @@
"category": "Hidden",
"description":
"""
OpenERP Web core module.
This module provides the core of the OpenERP Web Client.
OpenERP Web core module.
========================
This module provides the core of the OpenERP Web Client.
""",
"depends" : [],
'auto_install': True,

View File

@ -425,11 +425,14 @@ class DisableCacheMiddleware(object):
def start_wrapped(status, headers):
referer = environ.get('HTTP_REFERER', '')
parsed = urlparse.urlparse(referer)
debug = not urlparse.parse_qs(parsed.query).has_key('debug')
filtered_headers = [(k,v) for k,v in headers if not (k=='Last-Modified' or (debug and k=='Cache-Control'))]
debug = parsed.query.count('debug') >= 1
nh = dict(headers)
if 'Last-Modified' in nh: del nh['Last-Modified']
if debug:
filtered_headers.append(('Cache-Control', 'no-cache'))
start_response(status, filtered_headers)
if 'Expires' in nh: del nh['Expires']
if 'Etag' in nh: del nh['Etag']
nh['Cache-Control'] = 'no-cache'
start_response(status, nh.items())
return self.app(environ, start_wrapped)
class Root(object):

View File

@ -194,9 +194,12 @@ class WebClient(openerpweb.Controller):
if mods is not None:
path += '?mods=' + mods
return [path]
# old code to force cache reloading, this really works, sorry niv but we stop wasting time
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
return [el[1] for el in self.manifest_glob(req, mods, extension)]
no_sugar = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1
no_sugar = no_sugar or req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
if not no_sugar:
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
else:
return [el[1] for el in self.manifest_glob(req, mods, extension)]
@openerpweb.jsonrequest
def csslist(self, req, mods=None):
@ -994,6 +997,16 @@ class DataSet(openerpweb.Controller):
elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
kwargs[k] = req.session.eval_domain(kwargs[k])
# Temporary implements future display_name special field for model#read()
if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
if 'display_name' in args[1]:
names = req.session.model(model).name_get(args[0], **kwargs)
args[1].remove('display_name')
r = getattr(req.session.model(model), method)(*args, **kwargs)
for i in range(len(r)):
r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
return r
return getattr(req.session.model(model), method)(*args, **kwargs)
@openerpweb.jsonrequest

File diff suppressed because it is too large Load Diff

View File

@ -1320,6 +1320,9 @@
.openerp .oe_view_manager .oe_view_manager_pager {
line-height: 26px;
}
.openerp .oe_view_manager .oe_view_manager_pager .oe_list_pager_single_page .oe_pager_group {
display: none;
}
.openerp .oe_view_manager .oe_pager_value {
float: left;
margin-right: 8px;

View File

@ -1019,6 +1019,8 @@ $sheet-max-width: 860px
// ViewManager.pager {{{
.oe_view_manager_pager
line-height: 26px
.oe_list_pager_single_page .oe_pager_group
display: none
.oe_pager_value
float: left
margin-right: 8px

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.setElement(content || this.make(this.tagName));
this.dialog_options = {
modal: true,
destroy_on_close: true,
@ -584,25 +582,21 @@ instance.web.Login = instance.web.Widget.extend({
localStorage.setItem('last_password_login_success', '');
}
}
self.do_action("login_sucessful");
self.trigger('login_successful');
},function () {
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");
instance.web.LoginSuccessful = instance.web.Widget.extend({
init: function(parent) {
this._super(parent);
},
start: function() {
this.getParent().getParent().show_application();
},
});
instance.web.client_actions.add("login_sucessful", "instance.web.LoginSuccessful");
instance.web.Menu = instance.web.Widget.extend({
template: 'Menu',
init: function() {
@ -890,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() {
@ -906,7 +899,8 @@ instance.web.Client = instance.web.Widget.extend({
this.$element.on('click', '.oe_dropdown_toggle', function(ev) {
ev.preventDefault();
var $toggle = $(this);
var $menu = $toggle.find('.oe_dropdown_menu');
var $menu = $toggle.siblings('.oe_dropdown_menu');
$menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
var state = $menu.is('.oe_opened');
setTimeout(function() {
// Do not alter propagation
@ -923,8 +917,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() {
@ -979,10 +975,9 @@ instance.web.WebClient = instance.web.Client.extend({
};
},
show_login: function() {
var self = this;
self.$('.oe_topbar').hide();
self.action_manager.do_action("login");
//self.login.appendTo(self.$element);
this.$('.oe_topbar').hide();
this.action_manager.do_action("login");
this.action_manager.inner_widget.on('login_successful', this, this.show_application);
},
show_application: function() {
var self = this;

View File

@ -378,7 +378,8 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, {
instance.web.EventDispatcherMixin.init.call(this);
this.__getterSetterInternalMap = {};
},
set: function(map) {
set: function(map, options) {
options = options || {};
var self = this;
var changed = false;
_.each(map, function(val, key) {
@ -387,10 +388,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, {
return;
changed = true;
self.__getterSetterInternalMap[key] = val;
self.trigger("change:" + key, self, {
oldValue: tmp,
newValue: val
});
if (! options.silent)
self.trigger("change:" + key, self, {
oldValue: tmp,
newValue: val
});
});
if (changed)
self.trigger("change", self);
@ -489,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.
*
@ -515,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);
},
/**
@ -532,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);
@ -611,6 +604,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
* @returns {jQuery.Deferred}
*/
start: function() {
return $.when();
}
});
@ -671,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.
@ -687,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;
},
/**
@ -702,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

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

View File

@ -516,7 +516,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.$element.addClass('oe_focused');
},
childBlurred: function () {
this.$element.removeClass('oe_focused');
var val = this.$element.val();
this.$element.val('');
var complete = this.$element.data('autocomplete');
if ((val && complete.term === undefined) || complete.previous !== undefined) {
throw new Error("new jquery.ui version altering implementation" +
" details relied on");
}
delete complete.term;
this.$element.removeClass('oe_focused')
.trigger('blur');
},
/**
*
@ -652,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(
@ -852,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
*/
@ -1669,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;
@ -1712,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,
@ -1737,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

@ -33,6 +33,11 @@ instance.web.form.FieldManagerMixin = {
};
instance.web.views.add('form', 'instance.web.FormView');
/**
* Properties:
* - actual_mode: always "view", "edit" or "create". Read-only property. Determines
* the mode used by the view.
*/
instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
/**
* Indicates that this view is not searchable, and thus that no search
@ -55,6 +60,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
* @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
*/
init: function(parent, dataset, view_id, options) {
var self = this;
this._super(parent);
this.set_default_options(options);
this.dataset = dataset;
@ -82,13 +88,22 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
this.__blur_timeout = null;
this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
this.qweb = null; // A QWeb instance will be created if the view is a QWeb template
self.set({actual_mode: self.options.initial_mode});
this.has_been_loaded.then(function() {
self.on("change:actual_mode", self, self.check_actual_mode);
self.check_actual_mode();
self.on("change:actual_mode", self, self.init_pager);
self.init_pager();
});
},
destroy: function() {
_.each(this.get_widgets(), function(w) {
w.off('focused blurred');
w.destroy();
});
this.$element.off('.formBlur');
if (this.$element) {
this.$element.off('.formBlur');
}
this._super();
},
on_loaded: function(data) {
@ -125,17 +140,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
this.$buttons.on('click','.oe_form_button_save',this.on_button_save);
this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel);
this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
if (this.options.$pager) {
this.$pager.appendTo(this.options.$pager);
} else {
this.$element.find('.oe_form_pager').replaceWith(this.$pager);
}
this.$pager.on('click','a[data-pager-action]',function() {
var action = $(this).data('pager-action');
self.on_pager_action(action);
});
this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar');
if (!this.sidebar && this.options.$sidebar) {
this.sidebar = new instance.web.Sidebar(this);
@ -152,14 +156,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
// Add bounce effect on button 'Edit' when click on readonly page view.
this.$element.find(".oe_form_field, .oe_form_group_cell").on('click', function (e) {
if(self.get("mode") == "view") {
if(self.get("actual_mode") == "view") {
var $button = self.options.$buttons.find(".oe_form_button_edit");
$button.wrap('<div>').css('margin-right','4px').addClass('oe_left oe_bounce');
}
});
this.on("change:mode", this, this.switch_mode);
this.set({mode: this.options.initial_mode});
this.has_been_loaded.resolve();
return $.when();
},
@ -267,8 +269,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (this.$pager) {
this.$pager.show();
}
this.$element.show().css('visibility', 'hidden');
this.$element.add(this.$buttons).removeClass('oe_form_dirty');
this.$element.css('visibility', 'visible');
this._super();
var shown = this.has_been_loaded;
if (options.reload !== false) {
@ -277,16 +280,17 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
// null index means we should start a new record
return self.on_button_new();
}
return self.dataset.read_index(_.keys(self.fields_view.fields), {
context: { 'bin_size': true }
var fields = _.keys(self.fields_view.fields);
fields.push('display_name');
return self.dataset.read_index(fields, {
context: { 'bin_size': true, 'future_display_name' : true }
}).pipe(self.on_record_loaded);
});
}
return shown.pipe(function() {
if (options.editable) {
self.set({mode: "edit"});
self.to_edit_mode();
}
self.$element.css('visibility', 'visible');
});
},
do_hide: function () {
@ -309,7 +313,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return $.Deferred().reject();
}
this.datarecord = record;
this.set({ 'title' : record.id ? record.name : "New record" });
this._actualize_mode();
this.set({ 'title' : record.id ? record.display_name : "New record" });
if (this.qweb) {
this.kill_current_form();
@ -386,6 +391,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
this.reload();
}
},
init_pager: function() {
var self = this;
if (this.$pager)
this.$pager.remove();
if (this.get("actual_mode") === "create")
return;
this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
if (this.options.$pager) {
this.$pager.appendTo(this.options.$pager);
} else {
this.$element.find('.oe_form_pager').replaceWith(this.$pager);
}
this.$pager.on('click','a[data-pager-action]',function() {
var action = $(this).data('pager-action');
self.on_pager_action(action);
});
this.do_update_pager();
},
do_update_pager: function(hide_index) {
var index = hide_index ? '-' : this.dataset.index + 1;
this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end()
@ -585,9 +608,32 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return $.Deferred().reject();
}
},
switch_mode: function() {
/**
* Ask the view to switch to view mode if possible. The view may not do it
* if the current record is not yet saved. It will then stay in create mode.
*/
to_view_mode: function() {
this._actualize_mode("view");
},
/**
* Ask the view to switch to edit mode if possible. The view may not do it
* if the current record is not yet saved. It will then stay in create mode.
*/
to_edit_mode: function() {
this._actualize_mode("edit");
},
/**
* Reactualize actual_mode.
*/
_actualize_mode: function(switch_to) {
var mode = switch_to || this.get("actual_mode");
if (! this.datarecord.id)
mode = "create";
this.set({actual_mode: mode});
},
check_actual_mode: function(source, options) {
var self = this;
if(this.get("mode") == "view") {
if(this.get("actual_mode") === "view") {
self.$element.removeClass('oe_form_editable').addClass('oe_form_readonly');
self.$buttons.find('.oe_form_buttons_edit').hide();
self.$buttons.find('.oe_form_buttons_view').show();
@ -605,11 +651,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
});
var fields_order = self.fields_order.slice(0);
if (self.default_focus_field) {
fields_order.unshift(self.default_focus_field);
fields_order.unshift(self.default_focus_field.name);
}
for (var i = 0; i < fields_order.length; i += 1) {
var field = self.fields[fields_order[i]];
if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.focus() !== false) {
if (!field.get('effective_invisible') && !field.get('effective_readonly')) {
field.focus();
break;
}
}
@ -618,19 +665,19 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
on_button_save: function() {
var self = this;
return this.do_save().then(function(result) {
self.set({mode: "view"});
self.to_view_mode();
});
},
on_button_cancel: function(event) {
if (this.can_be_discarded()) {
this.set({mode: "view"});
this.to_view_mode();
this.on_record_loaded(this.datarecord);
}
return false;
},
on_button_new: function() {
var self = this;
this.set({mode: "edit"});
this.to_edit_mode();
return $.when(this.has_been_loaded).pipe(function() {
if (self.can_be_discarded()) {
return self.load_defaults();
@ -638,7 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
});
},
on_button_edit: function() {
return this.set({mode: "edit"});
return this.to_edit_mode();
},
on_button_create: function() {
this.dataset.index = null;
@ -651,7 +698,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
return self.on_created({ result : new_id });
}).then(function() {
return self.set({mode: "edit"});
return self.to_edit_mode();
}).then(function() {
def.resolve();
});
@ -813,8 +860,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (self.dataset.index == null || self.dataset.index < 0) {
return $.when(self.on_button_new());
} else {
return self.dataset.read_index(_.keys(self.fields_view.fields), {
context : { 'bin_size' : true }
var fields = _.keys(self.fields_view.fields);
fields.push('display_name');
return self.dataset.read_index(fields, {
context : { 'bin_size' : true, 'future_display_name' : true }
}).pipe(self.on_record_loaded);
}
});
@ -944,7 +993,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
register_field: function(field, name) {
this.fields[name] = field;
this.fields_order.push(name);
if (field.node.attrs.default_focus == '1') {
if (JSON.parse(field.node.attrs.default_focus || "0")) {
this.default_focus_field = field;
}
@ -966,7 +1015,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return this.fields_view.fields[field_name];
},
is_create_mode: function() {
return !this.datarecord.id;
return this.get("actual_mode") === "create";
},
open_translate_dialog: function(field) {
return this._super(field);
@ -1650,7 +1699,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
this._super(view, node);
this.force_disabled = false;
this.string = (this.node.attrs.string || '').replace(/_/g, '');
if (this.node.attrs.default_focus == '1') {
if (JSON.parse(this.node.attrs.default_focus || "0")) {
// TODO fme: provide enter key binding to widgets
this.view.default_focus_button = this;
}
@ -2019,7 +2068,7 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function() {
this.delay_focus(this.$element.find('input:first'));
this.$element.find('input:first')[0].focus();
}
});
@ -2992,6 +3041,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
}
if(view.view_type === "list") {
_.extend(view.options, {
addable: null,
selectable: self.multi_selection,
sortable: false,
import_enabled: false,
@ -2999,7 +3049,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
});
if (self.get("effective_readonly")) {
_.extend(view.options, {
addable: null,
deletable: null,
reorderable: false,
});
@ -3029,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();
@ -3424,6 +3472,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this._super.apply(this, arguments);
}
});
instance.web.form.One2ManyList = instance.web.ListView.List.extend({
pad_table_to: function (count) {
this._super(count > 0 ? count - 1 : 0);
// magical invocation of wtf does that do
if (this.view.o2m.get('effective_readonly')) {
return;
}
var self = this;
var columns = _(this.columns).filter(function (column) {
return column.invisible !== '1';
}).length;
if (this.options.selectable) { columns++; }
if (this.options.deletable) { columns++; }
var $cell = $('<td>', {
colspan: columns,
'class': 'oe_form_field_one2many_list_row_add'
}).text(_t("Add a row"))
.click(function (e) {
e.preventDefault();
e.stopPropagation();
self.view.do_add_record();
});
this.$current.append(
$('<tr>').append($cell))
}
});
instance.web.form.One2ManyFormView = instance.web.FormView.extend({
form_template: 'One2Many.formview',
@ -3551,12 +3627,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
render_value: function() {
var self = this;
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context());
var values = self.get("value")
var handle_names = function(data) {
var indexed = {};
_.each(data, function(el) {
indexed[el[0]] = el;
});
data = _.map(self.get("value"), function(el) { return indexed[el]; });
data = _.map(values, function(el) { return indexed[el]; });
if (! self.get("effective_readonly")) {
self.tags.containerElement().children().remove();
$("textarea", self.$element).css("padding-left", "3px");
@ -3565,8 +3642,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self.$element.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
}
};
if (! self.get('values') || self.get('values').length > 0) {
this._display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names);
if (! values || values.length > 0) {
this._display_orderer.add(dataset.name_get(values)).then(handle_names);
} else {
handle_names([]);
}
@ -4228,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
@ -4241,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

View File

@ -376,6 +376,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
var total = dataset.size();
var limit = this.limit() || total;
this.$pager.toggle(total !== 0);
this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit));
var spager = '-';
if (total) {
@ -932,7 +933,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
'[data-id=' + record.get('id') + ']');
var index = $row.data('index');
$row.remove();
self.refresh_zebra(index);
},
'reset': function () { return self.on_records_reset(); },
'change': function (event, record, attribute, value, old_value) {
@ -967,8 +967,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
'[data-id=' + previous_record.get('id') + ']');
$new_row.insertAfter($previous_sibling);
}
self.refresh_zebra(index, 1);
}
};
_(this.record_callbacks).each(function (callback, event) {
@ -1108,7 +1106,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
this.$current
.children('tr:not([data-id])').remove().end()
.append(new Array(count - this.records.length + 1).join(row));
this.refresh_zebra(this.records.length);
},
/**
* Gets the ids of all currently selected records, if any
@ -1182,25 +1179,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
render_cell: function () {
return self.render_cell.apply(self, arguments); }
});
},
/**
* Fixes fixes the even/odd classes
*
* @param {Number} [from_index] index from which to resequence
* @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table
*/
refresh_zebra: function (from_index, offset) {
offset = offset || 0;
from_index = from_index || 0;
var dom_offset = offset + from_index;
var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null;
this.$current.children(sel).each(function (i, e) {
var index = from_index + i;
// reset record-index accelerators on rows and even/odd
var even = index%2 === 0;
$(e).toggleClass('even', even)
.toggleClass('odd', !even);
});
}
});
instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{
@ -1533,8 +1511,6 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
}(dataset, record.get('id'), seq));
record.set('sequence', seq);
}
list.refresh_zebra();
}
});
},

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();
@ -425,26 +423,64 @@ openerp.web.list_editable = function (instance) {
keyup_ESCAPE: function () {
return this.cancel_edition();
},
/**
* Gets the selection range (start, end) for the provided element,
* returns ``null`` if it can't get a range.
*
* @private
*/
_text_selection_range: function (el) {
if (el.selectionStart !== undefined) {
var selectionStart;
try {
selectionStart = el.selectionStart;
} catch (e) {
// radio or checkbox throw on selectionStart access
return null;
}
if (selectionStart !== undefined) {
return {
start: el.selectionStart,
start: selectionStart,
end: el.selectionEnd
};
} else if(document.body.createTextRange) {
} else if (document.body.createTextRange) {
throw new Error("Implement text range handling for MSIE");
var sel = document.body.createTextRange();
if (sel.parentElement() === el) {
}
}
// Element without selection ranges (select, div/@contenteditable)
return null;
},
_text_cursor: function (el) {
var selection = this._text_selection_range(el);
if (selection.start !== selection.end) {
if (!selection) {
return null;
}
return selection.start;
if (selection.start !== selection.end) {
return {position: null, collapsed: false};
}
return {position: selection.start, collapsed: true};
},
/**
* Checks if the cursor is at the start of the provided el
*
* @param {HTMLInputElement | HTMLTextAreaElement}
* @returns {Boolean}
* @private
*/
_at_start: function (cursor, el) {
return cursor.collapsed && (cursor.position === 0);
},
/**
* Checks if the cursor is at the end of the provided el
*
* @param {HTMLInputElement | HTMLTextAreaElement}
* @returns {Boolean}
* @private
*/
_at_end: function (cursor, el) {
return cursor.collapsed && (cursor.position === el.value.length);
},
/**
* @param DOMEvent event
@ -454,10 +490,11 @@ openerp.web.list_editable = function (instance) {
*/
_key_move_record: function (event, record_direction, is_valid_move) {
if (!this.editor.is_editing('edit')) { return $.when(); }
// FIXME: assumes editable widgets are input-type elements
var index = this._text_cursor(event.target);
// If selecting or not at the start of the input
if (!is_valid_move(event.target, index)) { return $.when(); }
var cursor = this._text_cursor(event.target);
// if text-based input (has a cursor)
// and selecting (not collapsed) or not at a field boundary
// don't move to the next record
if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }
event.preventDefault();
var source_field = $(event.target).closest('[data-fieldname]')
@ -466,13 +503,15 @@ openerp.web.list_editable = function (instance) {
},
keydown_UP: function (e) {
return this._key_move_record(e, 'pred', function (el, index) {
return index === 0;
var self = this;
return this._key_move_record(e, 'pred', function (el, cursor) {
return self._at_start(cursor, el);
});
},
keydown_DOWN: function (e) {
return this._key_move_record(e, 'succ', function (el, index) {
return index === el.value.length;
var self = this;
return this._key_move_record(e, 'succ', function (el, cursor) {
return self._at_end(cursor, el);
});
},
@ -480,8 +519,8 @@ openerp.web.list_editable = function (instance) {
// If the cursor is at the beginning of the field
var source_field = $(e.target).closest('[data-fieldname]')
.attr('data-fieldname');
var index = this._text_cursor(e.target);
if (index !== 0) { return $.when(); }
var cursor = this._text_cursor(e.target);
if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }
var fields_order = this.editor.form.fields_order;
var field_index = _(fields_order).indexOf(source_field);
@ -504,8 +543,8 @@ openerp.web.list_editable = function (instance) {
// looking for new fields at the right
var source_field = $(e.target).closest('[data-fieldname]')
.attr('data-fieldname');
var index = this._text_cursor(e.target);
if (index !== e.target.value.length) { return $.when(); }
var cursor = this._text_cursor(e.target);
if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }
var fields_order = this.editor.form.fields_order;
var field_index = _(fields_order).indexOf(source_field);

View File

@ -214,21 +214,30 @@ instance.web.ActionManager = instance.web.Widget.extend({
this.dialog_stop();
this.clear_breadcrumbs();
},
ir_actions_act_window: function (action, on_close) {
var self = this;
if (_(['base.module.upgrade', 'base.setup.installer'])
.contains(action.res_model)) {
var old_close = on_close;
on_close = function () {
instance.webclient.do_reload().then(old_close);
};
do_ir_actions_common: function(action, on_close) {
var self = this, klass, widget, add_breadcrumb;
if (action.type === 'ir.actions.client') {
var ClientWidget = instance.web.client_actions.get_object(action.tag);
widget = new ClientWidget(this, action.params);
klass = 'oe_act_client';
add_breadcrumb = function() {
self.push_breadcrumb({
widget: widget,
title: action.name
});
}
} else {
widget = new instance.web.ViewManagerAction(this, action);
klass = 'oe_act_window';
add_breadcrumb = widget.proxy('add_breadcrumb');
}
if (action.target === 'new') {
if (this.dialog === null) {
// These buttons will be overwrited by <footer> if any
this.dialog = new instance.web.Dialog(this, {
buttons: { "Close": function() { $(this).dialog("close"); }},
dialogClass: 'oe_act_window'
dialogClass: klass
});
if(on_close)
this.dialog.on_close.add(on_close);
@ -236,7 +245,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
this.dialog_widget.destroy();
}
this.dialog.dialog_title = action.name;
this.dialog_widget = new instance.web.ViewManagerAction(this, action);
this.dialog_widget = widget;
this.dialog_widget.appendTo(this.dialog.$element);
this.dialog.open();
} else {
@ -247,11 +256,34 @@ instance.web.ActionManager = instance.web.Widget.extend({
});
}
this.inner_action = action;
var inner_widget = this.inner_widget = new instance.web.ViewManagerAction(this, action);
inner_widget.add_breadcrumb();
this.inner_widget = widget;
add_breadcrumb();
this.inner_widget.appendTo(this.$element);
}
},
ir_actions_act_window: function (action, on_close) {
var self = this;
if (_(['base.module.upgrade', 'base.setup.installer'])
.contains(action.res_model)) {
var old_close = on_close;
on_close = function () {
instance.webclient.do_reload().then(old_close);
};
}
if (action.target !== 'new') {
if(action.menu_id) {
this.dialog_stop();
return this.getParent().do_action(action, function () {
instance.webclient.menu.open_menu(action.menu_id);
});
}
}
return this.do_ir_actions_common(action, on_close);
},
ir_actions_client: function (action, on_close) {
return this.do_ir_actions_common(action, on_close);
},
ir_actions_act_window_close: function (action, on_closed) {
if (!this.dialog && on_closed) {
on_closed();
@ -267,18 +299,6 @@ instance.web.ActionManager = instance.web.Widget.extend({
self.do_action(action, on_closed)
});
},
ir_actions_client: function (action) {
this.dialog_stop();
var ClientWidget = instance.web.client_actions.get_object(action.tag);
this.inner_widget = new ClientWidget(this, action.params);
this.push_breadcrumb({
widget: this.inner_widget,
title: action.name
});
this.inner_action = action;
this.do_push_state({});
this.inner_widget.appendTo(this.$element);
},
ir_actions_report_xml: function(action, on_closed) {
var self = this;
instance.web.blockUI();
@ -403,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) {
@ -425,7 +445,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
}
});
});
return view_promise;
},
do_create_view: function(view_type) {
// Lazy loading of views
@ -485,6 +504,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
});
this.getParent().push_breadcrumb({
widget: this,
action: this.action,
show: function(index, $e) {
var view_to_select = views[index];
self.$element.show();
@ -493,9 +513,27 @@ instance.web.ViewManager = instance.web.Widget.extend({
}
},
get_title: function() {
return _.map(views, function(v) {
return self.views[v].controller.get('title');
var id;
var currentIndex;
_.each(self.getParent().breadcrumbs, function(bc, i) {
if (bc.widget === self) {
currentIndex = i;
}
});
var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0];
var titles = _.map(views, function(v) {
var controller = self.views[v].controller;
if (v === 'form') {
id = controller.datarecord.id;
}
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 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();
}
return titles;
}
});
},
@ -1136,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() {

View File

@ -518,25 +518,25 @@
<div class="oe_form_dropdown_section">
<button class="oe_dropdown_toggle oe_dropdown_arrow">
<t t-esc="section.label"/>
<ul class="oe_dropdown_menu">
<li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
<a class="oe_sidebar_action_a" t-att-title="item.title" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank">
<t t-raw="item.label"/>
</a>
<a t-if="section.name == 'files'" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment">x</a>
</li>
<li t-if="section.name == 'files'" class="oe_sidebar_add_attachment">
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t>
<input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/>
<input type="hidden" name="id" t-att-value="widget.model_id"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
<span>Add...</span>
</t>
</li>
</ul>
</button>
<ul class="oe_dropdown_menu">
<li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
<a class="oe_sidebar_action_a" t-att-title="item.title" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank">
<t t-raw="item.label"/>
</a>
<a t-if="section.name == 'files'" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment">x</a>
</li>
<li t-if="section.name == 'files'" class="oe_sidebar_add_attachment">
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t>
<input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/>
<input type="hidden" name="id" t-att-value="widget.model_id"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
<span>Add...</span>
</t>
</li>
</ul>
</div>
</t>
</div>
@ -645,10 +645,9 @@
<t t-name="ListView.rows" t-foreach="records.length" t-as="index">
<t t-call="ListView.row">
<t t-set="record" t-value="records.at(index)"/>
<t t-set="row_parity" t-value="index_parity"/>
</t>
</t>
<tr t-name="ListView.row" t-att-class="row_parity"
<tr t-name="ListView.row"
t-att-data-id="record.get('id')"
t-att-style="view.style_for(record)">
<t t-set="asData" t-value="record.toForm().data"/>
@ -1444,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">

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

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

@ -1,10 +1,7 @@
{
"name": "Web Calendar",
"category": "Hidden",
"description":
"""
OpenERP Web Calendar view.
""",
"description":"""OpenERP Web Calendar view.""",
"version": "2.0",
"depends": ['web'],
"js": [

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

@ -1,10 +1,7 @@
{
"name": "Web Dashboard",
"category": "Hidden",
"description":
"""
OpenERP Web Dashboard view.
""",
"description":"""OpenERP Web Dashboard view.""",
"version": "2.0",
"depends": ['web'],
"js": [

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

@ -1,7 +1,7 @@
{
"name" : "OpenERP Web Diagram",
"category" : "Hidden",
"description":'Openerp Web Diagram view',
"description":"""Openerp Web Diagram view.""",
"version" : "2.0",
"depends" : ["web"],
"js": [

View File

@ -1,10 +1,7 @@
{
"name": "Web Gantt",
"category": "Hidden",
"description":
"""
OpenERP Web Gantt chart view.
""",
"description":"""OpenERP Web Gantt chart view.""",
"version": "2.0",
"depends": ['web'],
"js": [

View File

@ -1,14 +1,16 @@
{
"name": "Graph Views",
"category" : "Hidden",
"description":"""Graph Views for Web Client
"description":"""
Graph Views for Web Client.
===========================
* Parse a <graph> view but allows changing dynamically the presentation
* Graph Types: pie, lines, areas, bars, radar
* Stacked/Not Stacked for areas and bars
* Legends: top, inside (top/left), hidden
* Features: download as PNG or CSV, browse data grid, switch orientation
* Unlimited "Group By" levels (not stacked), two cross level analysis (stacked)
* Parse a <graph> view but allows changing dynamically the presentation
* Graph Types: pie, lines, areas, bars, radar
* Stacked/Not Stacked for areas and bars
* Legends: top, inside (top/left), hidden
* Features: download as PNG or CSV, browse data grid, switch orientation
* Unlimited "Group By" levels (not stacked), two cross level analysis (stacked)
""",
"version": "3.0",
"depends": ['web'],

View File

@ -1,10 +1,7 @@
{
"name": "Hello",
"category": "Hidden",
"description":
"""
OpenERP Web example module.
""",
"description":"""OpenERP Web example module.""",
"version": "2.0",
"depends": [],
"js": ["static/*/*.js", "static/*/js/*.js"],

View File

@ -1,10 +1,7 @@
{
"name" : "Base Kanban",
"category": "Hidden",
"description":
"""
OpenERP Web kanban view.
""",
"description":"""OpenERP Web kanban view.""",
"version" : "2.0",
"depends" : ["web"],
"js": [

View File

@ -39,6 +39,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;
}
@ -64,7 +67,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;
@ -188,7 +191,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 {
@ -239,13 +242,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
@ -190,7 +192,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
@ -225,9 +227,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

@ -174,6 +174,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 = [];
@ -195,6 +196,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();
@ -296,10 +298,10 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
if (!group.state.folded) {
if (182*unfolded>=self.$element.width()) {
group.$element.css('width', "170px");
} else if (262*unfolded>self.$element.width()) {
group.$element.css('width', Math.round(100/unfolded) + '%');
} else {
} else if (262*unfolded<self.$element.width()) {
group.$element.css('width', "250px");
} else {
group.$element.css('width', Math.floor(self.$element.width()/unfolded) + 'px');
}
}
});
@ -721,9 +723,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();

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

@ -1,10 +1,7 @@
{
"name" : "OpenERP Web Mobile",
"category": "Hidden",
"description":
"""
OpenERP Web Mobile.
""",
"description":"""OpenERP Web Mobile.""",
"version" : "2.0",
"depends" : [],
'auto_install': True,

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

@ -1,10 +1,7 @@
{
"name" : "Process",
"version": "2.0",
"description":
"""
OpenERP Web process view.
""",
"description":"""OpenERP Web process view.""",
"depends" : ["web_diagram"],
"js": [
'static/lib/dracula/*.js',

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

@ -1,7 +1,7 @@
{
"name" : "OpenERP Web Web",
"category" : "Hidden",
"description":'Openerp Web Web',
"description":"""Openerp Web Web.""",
"version" : "2.0",
"depends" : [],
"installable" : False,

View File

@ -1,10 +1,7 @@
{
"name": "Tests",
"category": "Hidden",
"description":
"""
OpenERP Web test suite.
""",
"description":"""OpenERP Web test suite.""",
"version": "2.0",
"depends": [],
"js": ["static/src/js/*.js"],

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