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
`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:
@ -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
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
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
======================
@ -134,6 +140,11 @@ Known issues / Roadmap
* 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
===========

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) {
"use strict";
// heavily inspired by Odoo's `ListRenderer`
// Heavily inspired by Odoo's `ListRenderer`
var BasicRenderer = require('web.BasicRenderer');
var config = require('web.config');
var core = require('web.core');
var field_utils = require('web.field_utils');
var _t = core._t;
var FIELD_CLASSES = {
// copied from ListRenderer
// Copied from ListRenderer
float: 'o_list_number',
integer: '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) {
this._super.apply(this, arguments);
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
* @returns {String} a string with the generated html.
*
*/
_renderRows: function () {
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]));
var $cells = _.map(this.columns, function (node, 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;
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 value = record.data[this.matrix_data.field_y_axis];
if (value.type === 'record') {
// we have a related record
// We have a related record
value = value.data.display_name;
}
// get 1st column filled w/ Y label
// Get 1st column filled w/ Y label
$td.text(value);
return $td;
},
@ -272,7 +280,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (modifiers.invisible && !(options && options.renderInvisible)) {
return $td;
}
// enforce mode of the parent
// Enforce mode of the parent
options.mode = this.getParent().mode;
var widget = this._renderFieldWidget(
node, record, _.pick(options, 'mode')
@ -329,7 +337,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
return;
}
var type = field.type;
if (!_.inArray(type, ['integer', 'float', 'monetary'])) {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return;
}
_.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) {
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;
}
var type = field.type;
if (!_.inArray(type, ['integer', 'float', 'monetary'])) {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return;
}
_.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',
/**
* 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 {Object} record information about the database records.
* @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.
*/
init_params: function () {
@ -56,7 +56,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
node[property];
}
}
// and this?
// And this?
this.field_editability =
node.field_editability || this.field_editability;
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],
y = record.data[this.field_y_axis];
if (x.type === 'record') {
// we have a related record
// We have a related record
x = x.data.display_name;
}
if (y.type === 'record') {
// we have a related record
// We have a related record
y = y.data.display_name;
}
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_y_axis[y][x] = record;
}.bind(this));
// init columns
// Init columns
this.columns = [];
$.each(this.by_x_axis, function (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) {
return {
// simulate node parsed on xml arch
// Simulate node parsed on xml arch
'tag': 'field',
'attrs': {
'name': this.field_x_axis,
@ -137,7 +137,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
*/
_make_row: function (y) {
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': []};
$.each(self.by_x_axis, function (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
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, {
arch: arch,
editable: true,
viewType: viewType,
editable: this.mode === 'edit' && arch.attrs.editable,
viewType: "list",
matrix_data: this.matrix_data,
});
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);
},
/**
* 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);

Loading…
Cancel
Save