From f1278e4ba63514be21058a0e005fd72623484846 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Tue, 21 Aug 2018 12:08:28 +0100 Subject: [PATCH] [FIX] web_widget_x2many_2d_matrix: Enable keyboard navigation --- web_widget_x2many_2d_matrix/README.rst | 115 ++++++++++-------- .../static/src/js/2d_matrix_renderer.js | 72 +++++++++-- .../static/src/js/widget_x2many_2d_matrix.js | 54 +++++--- 3 files changed, 161 insertions(+), 80 deletions(-) diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst index aba2f90a..44f5920c 100644 --- a/web_widget_x2many_2d_matrix/README.rst +++ b/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: - - - - - - - - +.. code-block:: xml + + + + + + + + + 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:: - - - - - - - - - - +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 + + + + + + + + + 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 =========== diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js index 6cb66ac2..dcbc44ad 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js +++ b/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 = $(''); 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) { diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js index 420c624c..638c080a 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js +++ b/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);