From 905e01921f3c3ef43ace6fc537d1d8c0d280002c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 5 Mar 2017 10:05:56 +0100 Subject: [REF] web, *: redesign all JS views This commit introduce a full redesign of all JS views. We started basically from scratch. The goal was to unify all the various views under a common framework, to make them testable, to make then usable in different conditions (in studio, or in the frontend), and to make our lives easier. Some important points are: - we introduced new coding guidelines (camelCase, 80 chars width, ...) - we have a brand new testing framework (still QUnit based) - kanban view moved to the web addon - calendar view (formerly web_calender) moved to web as well - the tree view was removed - all new code should be documented We hope that this code is the start of a new era for the Odoo web client, we want to have a high quality codebase, well documented, well tested, well designed. Work done by the framework team: mostly aab, ged, chm, dmo, qsm --- addons/web/static/src/js/fields/basic_fields.js | 1300 +++++++++++++++++++++++ 1 file changed, 1300 insertions(+) create mode 100644 addons/web/static/src/js/fields/basic_fields.js (limited to 'addons/web/static/src/js/fields/basic_fields.js') diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js new file mode 100644 index 00000000000..d07bcb5dd95 --- /dev/null +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -0,0 +1,1300 @@ +odoo.define('web.basic_fields', function (require) { +"use strict"; + +/** + * This module contains most of the basic (meaning: non relational) field + * widgets. Field widgets are supposed to be used in views inheriting from + * BasicView, so, they can work with the records obtained from a BasicModel. + */ + +var AbstractField = require('web.AbstractField'); +var ajax = require('web.ajax'); +var config = require('web.config'); +var core = require('web.core'); +var crash_manager = require('web.crash_manager'); +var datepicker = require('web.datepicker'); +var dom = require('web.dom'); +var field_utils = require('web.field_utils'); +var framework = require('web.framework'); +var session = require('web.session'); +var utils = require('web.utils'); + +var qweb = core.qweb; +var _t = core._t; + + +var InputField = AbstractField.extend({ + events: _.extend({}, AbstractField.prototype.events, { + 'input': '_onInput', + }), + init: function () { + this._super.apply(this, arguments); + if (this.mode === 'edit') { + this.tagName = 'input'; + } + }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + activate: function () { + this.$el.focus(); + setTimeout(this.$el.select.bind(this.$el), 0); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _renderEdit: function () { + this.$el.addClass('o_form_input'); + this.$el.attr('type', 'text'); + if (this.attrs.placeholder) { + this.$el.attr('placeholder', this.attrs.placeholder); + } + this.$el.attr('id', this.idForLabel); + // save cursor position to restore it after updating value + var selectionStart = this.el.selectionStart; + var selectionEnd = this.el.selectionEnd; + this.$el.val(this._formatValue(this.value)); + this.el.selectionStart = selectionStart; + this.el.selectionEnd = selectionEnd; + }, + _renderReadonly: function () { + this.$el.empty().html(this._formatValue(this.value)); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Implement keyboard movements. Mostly useful for its environment, such + * as a list view. + * + * @override + * @param {any} event + */ + _onKeydown: function (event) { + var input = this.$el[0]; + var is_not_selecting; + switch (event.which) { + case $.ui.keyCode.DOWN: + this.trigger_up('move_down'); + break; + case $.ui.keyCode.UP: + this.trigger_up('move_up'); + break; + case $.ui.keyCode.LEFT: + is_not_selecting = input.selectionEnd === input.selectionStart; + if (is_not_selecting && input.selectionStart === 0) { + this.trigger_up('move_left'); + } + break; + case $.ui.keyCode.RIGHT: + is_not_selecting = input.selectionEnd === input.selectionStart; + if (is_not_selecting && input.selectionEnd === input.value.length) { + this.trigger_up('move_right'); + } + break; + case $.ui.keyCode.ENTER: + this.trigger_up('move_next_line'); + break; + } + this._super.apply(this, arguments); + }, + /** + * We notify the outside world for each change in this input value. It does + * not necessarily mean that onchanges will be triggered instantly. + */ + _onInput: function () { + this._setValue(this.$el.val()); + }, +}); + +var FieldChar = InputField.extend({ + supportedFieldTypes: ['char', 'text'], + tagName: 'span', +}); + +var FieldDate = InputField.extend({ + className: "o_form_field_date", + tagName: "span", + replace_element: true, + supportedFieldTypes: ['date', 'datetime'], + + start: function () { + var self = this; + var def; + if (this.mode === 'edit') { + this.datewidget = this._makeDatePicker(); + this.datewidget.on('datetime_changed', this, function () { + if (this.datewidget.get_value() !== this._parseValue(this.value)) { + this._setValue(this.datewidget.get_value()); + } + }); + def = this.datewidget.appendTo('
').done(function () { + self.datewidget.$el.addClass(self.$el.attr('class')); + self.datewidget.$input.addClass('o_form_input'); + self.datewidget.$input.attr('id', self.idForLabel); + self.replaceElement(self.datewidget.$el); + }); + } + return $.when(def, this._super.apply(this, arguments)); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + activate: function () { + var $input = this.datewidget.$input; + $input.focus(); + setTimeout($input.select.bind($input), 0); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _makeDatePicker: function () { + return new datepicker.DateWidget(this); + }, + _renderEdit: function () { + this.datewidget.set_value(this.value); + }, +}); + +var FieldDateTime = FieldDate.extend({ + supportedFieldTypes: ['datetime'], + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _makeDatePicker: function () { + return new datepicker.DateTimeWidget(this); + }, +}); + +var FieldMonetary = InputField.extend({ + className: 'o_list_number', + supportedFieldTypes: ['float', 'monetary'], + // FieldMonetary overrides _formatValue to ensure that the format_monetary + // method is used. This allows this widget to be used with other field + // types, such as float. + _formatValue: function (value) { + var options = _.extend({}, this.nodeOptions, { data: this.recordData }); + return field_utils.format_monetary(value, this.field, options); + }, + isSet: function () { + return this.value !== false; + } +}); + +var FieldBoolean = AbstractField.extend({ + className: 'o_field_boolean', + events: _.extend({}, AbstractField.prototype.events, { + change: '_onChange', + keydown: '_onKeyDown', + }), + replace_element: true, + supportedFieldTypes: ['boolean'], + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + activate: function () { + this.$input.focus(); + setTimeout(this.$input.select.bind(this.$input), 0); + }, + isSet: function () { + return true; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _renderReadonly: function () { + var $checkbox = this._formatValue(this.value); + this.$input = $checkbox.find('input'); + this.$el.empty().append($checkbox); + }, + _renderEdit: function () { + this._renderReadonly(); + this.$input.prop('disabled', false); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onChange: function () { + this._setValue(this.$input[0].checked); + }, + /** + * @param {KeyEvent} event + */ + _onKeydown: function (event) { + switch (event.which) { + case $.ui.keyCode.DOWN: + this.trigger_up('move_down'); + break; + case $.ui.keyCode.UP: + this.trigger_up('move_up'); + break; + case $.ui.keyCode.LEFT: + this.trigger_up('move_left'); + break; + case $.ui.keyCode.RIGHT: + this.trigger_up('move_right'); + break; + case $.ui.keyCode.TAB: + this.trigger_up('move_next'); + event.preventDefault(); + break; + case $.ui.keyCode.ENTER: + this.$input.prop('checked', !this.value); + this._setValue(!this.value); + break; + } + }, +}); + +var FieldInteger = InputField.extend({ + supportedFieldTypes: ['integer'], + isSet: function () { + return this.value !== false; + }, +}); + +var FieldFloat = InputField.extend({ + supportedFieldTypes: ['float'], + _renderReadonly: function () { + var precision; + var value = this.value; + if (this.attrs.digits) { + precision = this.node.attrs.digits; + } else { + precision = this.field.digits; + } + if (precision && precision.length === 2) { + value = utils.round_decimals(value, precision[1]); + } + var $span = $('').addClass('o_form_field o_form_field_number').text(this._formatValue(value)); + this.$el.html($span); + }, + isSet: function () { + return this.value !== ''; + }, +}); + +var FieldFloatTime = FieldFloat.extend({ + supportedFieldTypes: ['float'], + _formatValue: function (value) { + var pattern = '%02d:%02d'; + if (value < 0) { + value = Math.abs(value); + pattern = '-' + pattern; + } + var hour = Math.floor(value); + var min = Math.round((value % 1) * 60); + if (min === 60){ + min = 0; + hour = hour + 1; + } + return _.str.sprintf(pattern, hour, min); + }, + _parseValue: function (value) { + var factor = 1; + if (value[0] === '-') { + value = value.slice(1); + factor = -1; + } + var float_time_pair = value.split(":"); + if (float_time_pair.length !== 2) { + return factor * field_utils.parse_float(value, this.field); + } + var hours = field_utils.parse_integer(float_time_pair[0]); + var minutes = field_utils.parse_integer(float_time_pair[1]); + return factor * (hours + (minutes / 60)); + }, +}); + +var FieldText = AbstractField.extend({ + events: _.extend({}, AbstractField.prototype.events, { + 'input': function () { + this._setValue(this.$textarea.val()); + }, + }), + supportedFieldTypes: ['text'], + start: function () { + this.$el.addClass('o_list_text o_form_textarea'); + this.$el.attr('id', this.idForLabel); + + if (this.mode === 'edit') { + // in edit mode, the widget contains a textarea. We append it in + // start() instead of _renderEdit() to keep the same textarea even + // if several _renderEdit are done. This allows to keep the cursor + // position and to autoresize only once + this.$textarea = $('