Browse Source

[FIX] web_widget_x2many_2d_matrix: Enable keyboard navigation

pull/1106/head
Jairo Llopis 6 years ago
committed by Jairo Llopis
parent
commit
f1278e4ba6
  1. 115
      web_widget_x2many_2d_matrix/README.rst
  2. 72
      web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js
  3. 54
      web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js

115
web_widget_x2many_2d_matrix/README.rst

@ -39,16 +39,18 @@ Use this widget by saying::
This assumes that my_field refers to a model with the fields `x`, `y` and This assumes that my_field refers to a model with the fields `x`, `y` and
`value`. If your fields are named differently, pass the correct names as `value`. If your fields are named differently, pass the correct names as
attributes::
attributes:
<field name="my_field" widget="x2many_2d_matrix" field_x_axis="my_field1" field_y_axis="my_field2" field_value="my_field3">
<tree>
<field name="my_field"/>
<field name="my_field1"/>
<field name="my_field2"/>
<field name="my_field3"/>
</tree>
</field>
.. code-block:: xml
<field name="my_field" widget="x2many_2d_matrix" field_x_axis="my_field1" field_y_axis="my_field2" field_value="my_field3">
<tree>
<field name="my_field"/>
<field name="my_field1"/>
<field name="my_field2"/>
<field name="my_field3"/>
</tree>
</field>
You can pass the following parameters: You can pass the following parameters:
@ -80,49 +82,53 @@ You need a data structure already filled with values. Let's assume we want to
use this widget in a wizard that lets the user fill in planned hours for one use this widget in a wizard that lets the user fill in planned hours for one
task per project per user. In this case, we can use ``project.task`` as our task per project per user. In this case, we can use ``project.task`` as our
data model and point to it from our wizard. The crucial part is that we fill data model and point to it from our wizard. The crucial part is that we fill
the field in the default function::
from odoo import fields, models
class MyWizard(models.TransientModel):
_name = 'my.wizard'
def _default_task_ids(self):
# your list of project should come from the context, some selection
# in a previous wizard or wherever else
projects = self.env['project.project'].browse([1, 2, 3])
# same with users
users = self.env['res.users'].browse([1, 2, 3])
return [
(0, 0, {
'name': 'Sample task name',
'project_id': p.id,
'user_id': u.id,
'planned_hours': 0,
'message_needaction': False,
'date_deadline': fields.Date.today(),
})
# if the project doesn't have a task for the user, create a new one
if not p.task_ids.filtered(lambda x: x.user_id == u) else
# otherwise, return the task
(4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id)
for p in projects
for u in users
]
task_ids = fields.Many2many('project.task', default=_default_task_ids)
Now in our wizard, we can use::
<field name="task_ids" widget="x2many_2d_matrix" field_x_axis="project_id" field_y_axis="user_id" field_value="planned_hours">
<tree>
<field name="task_ids"/>
<field name="project_id"/>
<field name="user_id"/>
<field name="planned_hours"/>
</tree>
</field>
the field in the default function:
.. code-block:: python
from odoo import fields, models
class MyWizard(models.TransientModel):
_name = 'my.wizard'
def _default_task_ids(self):
# your list of project should come from the context, some selection
# in a previous wizard or wherever else
projects = self.env['project.project'].browse([1, 2, 3])
# same with users
users = self.env['res.users'].browse([1, 2, 3])
return [
(0, 0, {
'name': 'Sample task name',
'project_id': p.id,
'user_id': u.id,
'planned_hours': 0,
'message_needaction': False,
'date_deadline': fields.Date.today(),
})
# if the project doesn't have a task for the user,
# create a new one
if not p.task_ids.filtered(lambda x: x.user_id == u) else
# otherwise, return the task
(4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id)
for p in projects
for u in users
]
task_ids = fields.Many2many('project.task', default=_default_task_ids)
Now in our wizard, we can use:
.. code-block:: xml
<field name="task_ids" widget="x2many_2d_matrix" field_x_axis="project_id" field_y_axis="user_id" field_value="planned_hours">
<tree>
<field name="task_ids"/>
<field name="project_id"/>
<field name="user_id"/>
<field name="planned_hours"/>
</tree>
</field>
Known issues / Roadmap Known issues / Roadmap
====================== ======================
@ -134,6 +140,11 @@ Known issues / Roadmap
* Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901 * Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901
* Support cell traversal through keyboard arrows.
* Entering the widget from behind by pressing ``Shift+TAB`` in your keyboard
will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490
is merged.
Bug Tracker Bug Tracker
=========== ===========

72
web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js

@ -4,14 +4,14 @@
odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) {
"use strict"; "use strict";
// heavily inspired by Odoo's `ListRenderer`
// Heavily inspired by Odoo's `ListRenderer`
var BasicRenderer = require('web.BasicRenderer'); var BasicRenderer = require('web.BasicRenderer');
var config = require('web.config'); var config = require('web.config');
var core = require('web.core'); var core = require('web.core');
var field_utils = require('web.field_utils'); var field_utils = require('web.field_utils');
var _t = core._t; var _t = core._t;
var FIELD_CLASSES = { var FIELD_CLASSES = {
// copied from ListRenderer
// Copied from ListRenderer
float: 'o_list_number', float: 'o_list_number',
integer: 'o_list_number', integer: 'o_list_number',
monetary: 'o_list_number', monetary: 'o_list_number',
@ -26,9 +26,18 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
init: function (parent, state, params) { init: function (parent, state, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.editable = params.editable; this.editable = params.editable;
this.columns = params.matrix_data.columns;
this.rows = params.matrix_data.rows;
this.matrix_data = params.matrix_data;
this._saveMatrixData(params.matrix_data);
},
/**
* Update matrix data in current renderer instance.
*
* @param {Object} matrixData Contains the matrix data
*/
_saveMatrixData: function (matrixData) {
this.columns = matrixData.columns;
this.rows = matrixData.rows;
this.matrix_data = matrixData;
}, },
/** /**
@ -159,7 +168,6 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
* *
* @private * @private
* @returns {String} a string with the generated html. * @returns {String} a string with the generated html.
*
*/ */
_renderRows: function () { _renderRows: function () {
return _.map(this.rows, this._renderRow.bind(this)); return _.map(this.rows, this._renderRow.bind(this));
@ -181,7 +189,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
$tr = $tr.append(self._renderLabelCell(row.data[0])); $tr = $tr.append(self._renderLabelCell(row.data[0]));
var $cells = _.map(this.columns, function (node, index) { var $cells = _.map(this.columns, function (node, index) {
var record = row.data[index]; var record = row.data[index];
// make the widget use our field value for each cell
// Make the widget use our field value for each cell
node.attrs.name = self.matrix_data.field_value; node.attrs.name = self.matrix_data.field_value;
return self._renderBodyCell(record, node, index, {mode:''}); return self._renderBodyCell(record, node, index, {mode:''});
}); });
@ -203,10 +211,10 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
var $td = $('<td>'); var $td = $('<td>');
var value = record.data[this.matrix_data.field_y_axis]; var value = record.data[this.matrix_data.field_y_axis];
if (value.type === 'record') { if (value.type === 'record') {
// we have a related record
// We have a related record
value = value.data.display_name; value = value.data.display_name;
} }
// get 1st column filled w/ Y label
// Get 1st column filled w/ Y label
$td.text(value); $td.text(value);
return $td; return $td;
}, },
@ -272,7 +280,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (modifiers.invisible && !(options && options.renderInvisible)) { if (modifiers.invisible && !(options && options.renderInvisible)) {
return $td; return $td;
} }
// enforce mode of the parent
// Enforce mode of the parent
options.mode = this.getParent().mode; options.mode = this.getParent().mode;
var widget = this._renderFieldWidget( var widget = this._renderFieldWidget(
node, record, _.pick(options, 'mode') node, record, _.pick(options, 'mode')
@ -329,7 +337,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
return; return;
} }
var type = field.type; var type = field.type;
if (!_.inArray(type, ['integer', 'float', 'monetary'])) {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return; return;
} }
_.each(this.columns, function (column, index) { _.each(this.columns, function (column, index) {
@ -342,7 +350,45 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
_.each(this.rows, function (row) { _.each(this.rows, function (row) {
column.aggregate.value += row.data[index].data[fname]; column.aggregate.value += row.data[index].data[fname];
}); });
});
}.bind(this));
},
/**
* @override
*/
updateState: function (state, params) {
if (params.matrix_data) {
this._saveMatrixData(params.matrix_data);
}
return this._super.apply(this, arguments);
},
/**
* Traverse the fields matrix with the keyboard
*
* @override
* @private
* @param {OdooEvent} event "navigation_move" event
*/
_onNavigationMove: function (event) {
var widgets = this.__parentedChildren,
index = widgets.indexOf(event.target),
first = index === 0,
last = index === widgets.length - 1,
move = 0;
// Guess if we have to move the focus
if (event.data.direction === "next" && !last) {
move = 1;
} else if (event.data.direction === "previous" && !first) {
move = -1;
}
// Move focus
if (move) {
var target = widgets[index + move];
index = this.allFieldWidgets[target.record.id].indexOf(target);
this._activateFieldWidget(target.record, index, {inc: 0});
event.stopPropagation();
}
}, },
/** /**
@ -362,7 +408,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
return; return;
} }
var type = field.type; var type = field.type;
if (!_.inArray(type, ['integer', 'float', 'monetary'])) {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return; return;
} }
_.each(this.rows, function (row) { _.each(this.rows, function (row) {

54
web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js

@ -16,9 +16,9 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
widget_class: 'o_form_field_x2many_2d_matrix', widget_class: 'o_form_field_x2many_2d_matrix',
/** /**
* Initialize the widget & parameters.
*Initialize the widget & parameters.
* *
* @param {Object} parent contains the form view.
*@param {Object} parent contains the form view.
* @param {String} name the name of the field. * @param {String} name the name of the field.
* @param {Object} record information about the database records. * @param {Object} record information about the database records.
* @param {Object} options view options. * @param {Object} options view options.
@ -29,7 +29,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
}, },
/** /**
* Initialize the widget specific parameters.
*Initialize the widget specific parameters.
* Sets the axis and the values. * Sets the axis and the values.
*/ */
init_params: function () { init_params: function () {
@ -56,7 +56,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
node[property]; node[property];
} }
} }
// and this?
// And this?
this.field_editability = this.field_editability =
node.field_editability || this.field_editability; node.field_editability || this.field_editability;
this.show_row_totals = this.show_row_totals =
@ -80,11 +80,11 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
var x = record.data[this.field_x_axis], var x = record.data[this.field_x_axis],
y = record.data[this.field_y_axis]; y = record.data[this.field_y_axis];
if (x.type === 'record') { if (x.type === 'record') {
// we have a related record
// We have a related record
x = x.data.display_name; x = x.data.display_name;
} }
if (y.type === 'record') { if (y.type === 'record') {
// we have a related record
// We have a related record
y = y.data.display_name; y = y.data.display_name;
} }
this.by_x_axis[x] = this.by_x_axis[x] || {}; this.by_x_axis[x] = this.by_x_axis[x] || {};
@ -92,7 +92,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
this.by_x_axis[x][y] = record; this.by_x_axis[x][y] = record;
this.by_y_axis[y][x] = record; this.by_y_axis[y][x] = record;
}.bind(this)); }.bind(this));
// init columns
// Init columns
this.columns = []; this.columns = [];
$.each(this.by_x_axis, function (x) { $.each(this.by_x_axis, function (x) {
this.columns.push(this._make_column(x)); this.columns.push(this._make_column(x));
@ -120,7 +120,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
*/ */
_make_column: function (x) { _make_column: function (x) {
return { return {
// simulate node parsed on xml arch
// Simulate node parsed on xml arch
'tag': 'field', 'tag': 'field',
'attrs': { 'attrs': {
'name': this.field_x_axis, 'name': this.field_x_axis,
@ -137,7 +137,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
*/ */
_make_row: function (y) { _make_row: function (y) {
var self = this; var self = this;
// use object so that we can attach more data if needed
// Use object so that we can attach more data if needed
var row = {'data': []}; var row = {'data': []};
$.each(self.by_x_axis, function (x) { $.each(self.by_x_axis, function (x) {
row.data.push(self.by_y_axis[y][x]); row.data.push(self.by_y_axis[y][x]);
@ -170,21 +170,45 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
} }
// Ensure widget is re initiated when rendering // Ensure widget is re initiated when rendering
this.init_matrix(); this.init_matrix();
var arch = this.view.arch,
viewType = 'list';
var arch = this.view.arch;
// Update existing renderer
if (!_.isUndefined(this.renderer)) {
return this.renderer.updateState(this.value, {
matrix_data: this.matrix_data,
});
}
// Create a new matrix renderer
this.renderer = new X2Many2dMatrixRenderer(this, this.value, { this.renderer = new X2Many2dMatrixRenderer(this, this.value, {
arch: arch, arch: arch,
editable: true,
viewType: viewType,
editable: this.mode === 'edit' && arch.attrs.editable,
viewType: "list",
matrix_data: this.matrix_data, matrix_data: this.matrix_data,
}); });
this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix'); this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix');
// Remove previous rendered and add the newly created one
this.$el.find('div:not(.o_x2m_control_panel)').remove();
return this.renderer.appendTo(this.$el); return this.renderer.appendTo(this.$el);
},
/**
* Activate the widget.
*
* @override
*/
activate: function (options) {
// Won't work fine without https://github.com/odoo/odoo/pull/26490
this._backwards = options.event.data.direction === "previous";
var result = this._super.apply(this, arguments);
delete this._backwards;
return result;
}, },
/**
* Get first element to focus.
*
* @override
*/
getFocusableElement: function () {
return this.$(".o_input:" + (this._backwards ? "last" : "first"));
},
}); });
field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix); field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix);

Loading…
Cancel
Save