1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
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<String>
*/
supportedFieldTypes: [],
/**
* To override to give a user friendly name to the widget.
*
* @type <string>
*/
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 <button> triggers two events: a 'keydown' AND a 'click'
// - When creating and opening a dialog, the focus is automatically given to the primary button
// The end result caused some issues where a modal opened by an ENTER keypress (e.g. saving
// changes in multiple edition) confirmed the modal without any intentionnal user input.
ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'next_line'});
break;
case $.ui.keyCode.ESCAPE:
this.trigger_up('navigation_move', {direction: 'cancel', originalEvent: ev});
break;
case $.ui.keyCode.UP:
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'up'});
break;
case $.ui.keyCode.RIGHT:
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'right'});
break;
case $.ui.keyCode.DOWN:
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'down'});
break;
case $.ui.keyCode.LEFT:
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'left'});
break;
}
},
/**
* Updates the target data value with the current AbstractField instance.
* This allows to consider the parent field in case of nested fields. The
* field which triggered the event is still accessible through ev.target.
*
* @private
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
ev.data.target = this;
},
});
return AbstractField;
});
|