From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/web/static/src/js/fields/abstract_field.js | 621 ++++ .../web/static/src/js/fields/abstract_field_owl.js | 648 ++++ addons/web/static/src/js/fields/basic_fields.js | 3757 ++++++++++++++++++++ .../web/static/src/js/fields/basic_fields_owl.js | 132 + addons/web/static/src/js/fields/field_registry.js | 101 + .../web/static/src/js/fields/field_registry_owl.js | 26 + addons/web/static/src/js/fields/field_utils.js | 762 ++++ addons/web/static/src/js/fields/field_wrapper.js | 157 + .../web/static/src/js/fields/relational_fields.js | 3460 ++++++++++++++++++ addons/web/static/src/js/fields/signature.js | 173 + addons/web/static/src/js/fields/special_fields.js | 262 ++ addons/web/static/src/js/fields/upgrade_fields.js | 199 ++ 12 files changed, 10298 insertions(+) create mode 100644 addons/web/static/src/js/fields/abstract_field.js create mode 100644 addons/web/static/src/js/fields/abstract_field_owl.js create mode 100644 addons/web/static/src/js/fields/basic_fields.js create mode 100644 addons/web/static/src/js/fields/basic_fields_owl.js create mode 100644 addons/web/static/src/js/fields/field_registry.js create mode 100644 addons/web/static/src/js/fields/field_registry_owl.js create mode 100644 addons/web/static/src/js/fields/field_utils.js create mode 100644 addons/web/static/src/js/fields/field_wrapper.js create mode 100644 addons/web/static/src/js/fields/relational_fields.js create mode 100644 addons/web/static/src/js/fields/signature.js create mode 100644 addons/web/static/src/js/fields/special_fields.js create mode 100644 addons/web/static/src/js/fields/upgrade_fields.js (limited to 'addons/web/static/src/js/fields') diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js new file mode 100644 index 00000000..16e928ea --- /dev/null +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -0,0 +1,621 @@ +odoo.define('web.AbstractField', function (require) { +"use strict"; + +/** + * This is the basic field widget used by all the views to render a field in a view. + * These field widgets are mostly common to all views, in particular form and list + * views. + * + * The responsabilities of a field widget are mainly: + * - render a visual representation of the current value of a field + * - that representation is either in 'readonly' or in 'edit' mode + * - notify the rest of the system when the field has been changed by + * the user (in edit mode) + * + * Notes + * - the widget is not supposed to be able to switch between modes. If another + * mode is required, the view will take care of instantiating another widget. + * - notify the system when its value has changed and its mode is changed to 'readonly' + * - notify the system when some action has to be taken, such as opening a record + * - the Field widget should not, ever, under any circumstance, be aware of + * its parent. The way it communicates changes with the rest of the system is by + * triggering events (with trigger_up). These events bubble up and are interpreted + * by the most appropriate parent. + * + * Also, in some cases, it may not be practical to have the same widget for all + * views. In that situation, you can have a 'view specific widget'. Just register + * the widget in the registry prefixed by the view type and a dot. So, for example, + * a form specific many2one widget should be registered as 'form.many2one'. + * + * @module web.AbstractField + */ + +var field_utils = require('web.field_utils'); +var Widget = require('web.Widget'); + +var AbstractField = Widget.extend({ + events: { + 'keydown': '_onKeydown', + }, + custom_events: { + navigation_move: '_onNavigationMove', + }, + + /** + * An object representing fields to be fetched by the model eventhough not present in the view + * This object contains "field name" as key and an object as value. + * That value object must contain the key "type" + * see FieldBinaryImage for an example. + */ + fieldDependencies: {}, + + /** + * If this flag is set to true, the field widget will be reset on every + * change which is made in the view (if the view supports it). This is + * currently a form view feature. + */ + resetOnAnyFieldChange: false, + /** + * If this flag is given a string, the related BasicModel will be used to + * initialize specialData the field might need. This data will be available + * through this.record.specialData[this.name]. + * + * @see BasicModel._fetchSpecialData + */ + specialData: false, + /** + * to override to indicate which field types are supported by the widget + * + * @type Array + */ + supportedFieldTypes: [], + + /** + * To override to give a user friendly name to the widget. + * + * @type + */ + description: "", + /** + * Currently only used in list view. + * If this flag is set to true, the list column name will be empty. + */ + noLabel: false, + /** + * Currently only used in list view. + * If set, this value will be displayed as column name. + */ + label: '', + /** + * Abstract field class + * + * @constructor + * @param {Widget} parent + * @param {string} name The field name defined in the model + * @param {Object} record A record object (result of the get method of + * a basic model) + * @param {Object} [options] + * @param {string} [options.mode=readonly] should be 'readonly' or 'edit' + */ + init: function (parent, name, record, options) { + this._super(parent); + options = options || {}; + + // 'name' is the field name displayed by this widget + this.name = name; + + // the datapoint fetched from the model + this.record = record; + + // the 'field' property is a description of all the various field properties, + // such as the type, the comodel (relation), ... + this.field = record.fields[name]; + + // the 'viewType' is the type of the view in which the field widget is + // instantiated. For standalone widgets, a 'default' viewType is set. + this.viewType = options.viewType || 'default'; + + // the 'attrs' property contains the attributes of the xml 'field' tag, + // the inner views... + var fieldsInfo = record.fieldsInfo[this.viewType]; + this.attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {}; + + // the 'additionalContext' property contains the attributes to pass through the context. + this.additionalContext = options.additionalContext || {}; + + // this property tracks the current (parsed if needed) value of the field. + // Note that we don't use an event system anymore, using this.get('value') + // is no longer valid. + this.value = record.data[name]; + + // recordData tracks the values for the other fields for the same record. + // note that it is expected to be mostly a readonly property, you cannot + // use this to try to change other fields value, this is not how it is + // supposed to work. Also, do not use this.recordData[this.name] to get + // the current value, this could be out of sync after a _setValue. + this.recordData = record.data; + + // the 'string' property is a human readable (and translated) description + // of the field. Mostly useful to be displayed in various places in the + // UI, such as tooltips or create dialogs. + this.string = this.attrs.string || this.field.string || this.name; + + // Widget can often be configured in the 'options' attribute in the + // xml 'field' tag. These options are saved (and evaled) in nodeOptions + this.nodeOptions = this.attrs.options || {}; + + // dataPointID is the id corresponding to the current record in the model. + // Its intended use is to be able to tag any messages going upstream, + // so the view knows which records was changed for example. + this.dataPointID = record.id; + + // this is the res_id for the record in database. Obviously, it is + // readonly. Also, when the user is creating a new record, there is + // no res_id. When the record will be created, the field widget will + // be destroyed (when the form view switches to readonly mode) and a new + // widget with a res_id in mode readonly will be created. + this.res_id = record.res_id; + + // useful mostly to trigger rpcs on the correct model + this.model = record.model; + + // a widget can be in two modes: 'edit' or 'readonly'. This mode should + // never be changed, if a view changes its mode, it will destroy and + // recreate a new field widget. + this.mode = options.mode || "readonly"; + + // this flag tracks if the widget is in a valid state, meaning that the + // current value represented in the DOM is a value that can be parsed + // and saved. For example, a float field can only use a number and not + // a string. + this._isValid = true; + + // this is the last value that was set by the user, unparsed. This is + // used to avoid setting the value twice in a row with the exact value. + this.lastSetValue = undefined; + + // formatType is used to determine which format (and parse) functions + // to call to format the field's value to insert into the DOM (typically + // put into a span or an input), and to parse the value from the input + // to send it to the server. These functions are chosen according to + // the 'widget' attrs if is is given, and if it is a valid key, with a + // fallback on the field type, ensuring that the value is formatted and + // displayed according to the chosen widget, if any. + this.formatType = this.attrs.widget in field_utils.format ? + this.attrs.widget : + this.field.type; + // formatOptions (resp. parseOptions) is a dict of options passed to + // calls to the format (resp. parse) function. + this.formatOptions = {}; + this.parseOptions = {}; + + // if we add decorations, we need to reevaluate the field whenever any + // value from the record is changed + if (this.attrs.decorations) { + this.resetOnAnyFieldChange = true; + } + }, + /** + * When a field widget is appended to the DOM, its start method is called, + * and will automatically call render. Most widgets should not override this. + * + * @returns {Promise} + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$el.attr('name', self.name); + self.$el.addClass('o_field_widget'); + return self._render(); + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Activates the field widget. By default, activation means focusing and + * selecting (if possible) the associated focusable element. The selecting + * part can be disabled. In that case, note that the focused input/textarea + * will have the cursor at the very end. + * + * @param {Object} [options] + * @param {boolean} [options.noselect=false] if false and the input + * is of type text or textarea, the content will also be selected + * @param {Event} [options.event] the event which fired this activation + * @returns {boolean} true if the widget was activated, false if the + * focusable element was not found or invisible + */ + activate: function (options) { + if (this.isFocusable()) { + var $focusable = this.getFocusableElement(); + $focusable.focus(); + if ($focusable.is('input[type="text"], textarea')) { + $focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length; + if (options && !options.noselect) { + $focusable.select(); + } + } + return true; + } + return false; + }, + /** + * This function should be implemented by widgets that are not able to + * notify their environment when their value changes (maybe because their + * are not aware of the changes) or that may have a value in a temporary + * state (maybe because some action should be performed to validate it + * before notifying it). This is typically called before trying to save the + * widget's value, so it should call _setValue() to notify the environment + * if the value changed but was not notified. + * + * @abstract + * @returns {Promise|undefined} + */ + commitChanges: function () {}, + /** + * Returns the main field's DOM element (jQuery form) which can be focused + * by the browser. + * + * @returns {jQuery} main focusable element inside the widget + */ + getFocusableElement: function () { + return $(); + }, + /** + * Returns whether or not the field is empty and can thus be hidden. This + * method is typically called when the widget is in readonly, to hide it + * (and its label) if it is empty. + * + * @returns {boolean} + */ + isEmpty: function () { + return !this.isSet(); + }, + /** + * Returns true iff the widget has a visible element that can take the focus + * + * @returns {boolean} + */ + isFocusable: function () { + var $focusable = this.getFocusableElement(); + return $focusable.length && $focusable.is(':visible'); + }, + /** + * this method is used to determine if the field value is set to a meaningful + * value. This is useful to determine if a field should be displayed as empty + * + * @returns {boolean} + */ + isSet: function () { + return !!this.value; + }, + /** + * A field widget is valid if it was checked as valid the last time its + * value was changed by the user. This is checked before saving a record, by + * the view. + * + * Note: this is the responsibility of the view to check that required + * fields have a set value. + * + * @returns {boolean} true/false if the widget is valid + */ + isValid: function () { + return this._isValid; + }, + /** + * this method is supposed to be called from the outside of field widgets. + * The typical use case is when an onchange has changed the widget value. + * It will reset the widget to the values that could have changed, then will + * rerender the widget. + * + * @param {any} record + * @param {OdooEvent} [event] an event that triggered the reset action. It + * is optional, and may be used by a widget to share information from the + * moment a field change event is triggered to the moment a reset + * operation is applied. + * @returns {Promise} A promise, which resolves when the widget rendering + * is complete + */ + reset: function (record, event) { + this._reset(record, event); + return this._render() || Promise.resolve(); + }, + /** + * Remove the invalid class on a field + */ + removeInvalidClass: function () { + this.$el.removeClass('o_field_invalid'); + this.$el.removeAttr('aria-invalid'); + }, + /** + * Sets the given id on the focusable element of the field and as 'for' + * attribute of potential internal labels. + * + * @param {string} id + */ + setIDForLabel: function (id) { + this.getFocusableElement().attr('id', id); + }, + /** + * add the invalid class on a field + */ + setInvalidClass: function () { + this.$el.addClass('o_field_invalid'); + this.$el.attr('aria-invalid', 'true'); + }, + /** + * Update the modifiers with the newest value. + * Now this.attrs.modifiersValue can be used consistantly even with + * conditional modifiers inside field widgets, and without needing new + * events or synchronization between the widgets, renderer and controller + * + * @param {Object | null} modifiers the updated modifiers + * @override + */ + updateModifiersValue: function(modifiers) { + this.attrs.modifiersValue = modifiers || {}; + }, + + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Apply field decorations (only if field-specific decorations have been + * defined in an attribute). + * + * @private + */ + _applyDecorations: function () { + var self = this; + this.attrs.decorations.forEach(function (dec) { + var isToggled = py.PY_isTrue( + py.evaluate(dec.expression, self.record.evalContext) + ); + const className = self._getClassFromDecoration(dec.name); + self.$el.toggleClass(className, isToggled); + }); + }, + /** + * Converts the value from the field to a string representation. + * + * @private + * @param {any} value (from the field type) + * @param {string} [formatType=this.formatType] the formatter to use + * @returns {string} + */ + _formatValue: function (value, formatType) { + var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions); + return field_utils.format[formatType || this.formatType](value, this.field, options); + }, + /** + * Returns the className corresponding to a given decoration. A + * decoration is of the form 'decoration-%s'. By default, replaces + * 'decoration' by 'text'. + * + * @private + * @param {string} decoration must be of the form 'decoration-%s' + * @returns {string} + */ + _getClassFromDecoration: function (decoration) { + return `text-${decoration.split('-')[1]}`; + }, + /** + * Compares the given value with the last value that has been set. + * Note that we compare unparsed values. Handles the special case where no + * value has been set yet, and the given value is the empty string. + * + * @private + * @param {any} value + * @returns {boolean} true iff values are the same + */ + _isLastSetValue: function (value) { + return this.lastSetValue === value || (this.value === false && value === ''); + }, + /** + * This method check if a value is the same as the current value of the + * field. For example, a fieldDate widget might want to use the moment + * specific value isSame instead of ===. + * + * This method is used by the _setValue method. + * + * @private + * @param {any} value + * @returns {boolean} + */ + _isSameValue: function (value) { + return this.value === value; + }, + /** + * Converts a string representation to a valid value. + * + * @private + * @param {string} value + * @returns {any} + */ + _parseValue: function (value) { + return field_utils.parse[this.formatType](value, this.field, this.parseOptions); + }, + /** + * main rendering function. Override this if your widget has the same render + * for each mode. Note that this function is supposed to be idempotent: + * the result of calling 'render' twice is the same as calling it once. + * Also, the user experience will be better if your rendering function is + * synchronous. + * + * @private + * @returns {Promise|undefined} + */ + _render: function () { + if (this.attrs.decorations) { + this._applyDecorations(); + } + if (this.mode === 'edit') { + return this._renderEdit(); + } else if (this.mode === 'readonly') { + return this._renderReadonly(); + } + }, + /** + * Render the widget in edit mode. The actual implementation is left to the + * concrete widget. + * + * @private + * @returns {Promise|undefined} + */ + _renderEdit: function () { + }, + /** + * Render the widget in readonly mode. The actual implementation is left to + * the concrete widget. + * + * @private + * @returns {Promise|undefined} + */ + _renderReadonly: function () { + }, + /** + * pure version of reset, can be overridden, called before render() + * + * @private + * @param {any} record + * @param {OdooEvent} event the event that triggered the change + */ + _reset: function (record, event) { + this.lastSetValue = undefined; + this.record = record; + this.value = record.data[this.name]; + this.recordData = record.data; + }, + /** + * this method is called by the widget, to change its value and to notify + * the outside world of its new state. This method also validates the new + * value. Note that this method does not rerender the widget, it should be + * handled by the widget itself, if necessary. + * + * @private + * @param {any} value + * @param {Object} [options] + * @param {boolean} [options.doNotSetDirty=false] if true, the basic model + * will not consider that this field is dirty, even though it was changed. + * Please do not use this flag unless you really need it. Our only use + * case is currently the pad widget, which does a _setValue in the + * renderEdit method. + * @param {boolean} [options.notifyChange=true] if false, the basic model + * will not notify and not trigger the onchange, even though it was changed. + * @param {boolean} [options.forceChange=false] if true, the change event will be + * triggered even if the new value is the same as the old one + * @returns {Promise} + */ + _setValue: function (value, options) { + // we try to avoid doing useless work, if the value given has not changed. + if (this._isLastSetValue(value)) { + return Promise.resolve(); + } + this.lastSetValue = value; + try { + value = this._parseValue(value); + this._isValid = true; + } catch (e) { + this._isValid = false; + this.trigger_up('set_dirty', {dataPointID: this.dataPointID}); + return Promise.reject({message: "Value set is not valid"}); + } + if (!(options && options.forceChange) && this._isSameValue(value)) { + return Promise.resolve(); + } + var self = this; + return new Promise(function (resolve, reject) { + var changes = {}; + changes[self.name] = value; + self.trigger_up('field_changed', { + dataPointID: self.dataPointID, + changes: changes, + viewType: self.viewType, + doNotSetDirty: options && options.doNotSetDirty, + notifyChange: !options || options.notifyChange !== false, + allowWarning: options && options.allowWarning, + onSuccess: resolve, + onFailure: reject, + }); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Intercepts navigation keyboard events to prevent their default behavior + * and notifies the view so that it can handle it its own way. + * + * Note: the navigation keyboard events are stopped so that potential parent + * abstract field does not trigger the navigation_move event a second time. + * However, this might be controversial, we might wanna let the event + * continue its propagation and flag it to say that navigation has already + * been handled (TODO ?). + * + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + switch (ev.which) { + case $.ui.keyCode.TAB: + var event = this.trigger_up('navigation_move', { + direction: ev.shiftKey ? 'previous' : 'next', + }); + if (event.is_stopped()) { + ev.preventDefault(); + ev.stopPropagation(); + } + break; + case $.ui.keyCode.ENTER: + // We preventDefault the ENTER key because of two coexisting behaviours: + // - In HTML5, pressing ENTER on a