[MERGE] forward port of branch 7.0 up to e5533d0
[odoo/odoo.git] / addons / web / static / src / js / view_list_editable.js
1 /**
2  * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
3  * @namespace
4  */
5 (function() {
6
7     var instance = openerp;
8     openerp.web.list_editable = {};
9     var _t = instance.web._t;
10
11     // editability status of list rows
12     instance.web.ListView.prototype.defaults.editable = null;
13
14     // TODO: not sure second @lends on existing item is correct, to check
15     instance.web.ListView.include(/** @lends instance.web.ListView# */{
16         init: function () {
17             var self = this;
18             this._super.apply(this, arguments);
19
20             this.saving_mutex = new $.Mutex();
21
22             this._force_editability = null;
23             this._context_editable = false;
24             this.editor = this.make_editor();
25             // Stores records of {field, cell}, allows for re-rendering fields
26             // depending on cell state during and after resize events
27             this.fields_for_resize = [];
28             instance.web.bus.on('resize', this, this.resize_fields);
29
30             $(this.groups).bind({
31                 'edit': function (e, id, dataset) {
32                     self.do_edit(dataset.index, id, dataset);
33                 },
34                 'saved': function () {
35                     if (self.groups.get_selection().length) {
36                         return;
37                     }
38                     self.configure_pager(self.dataset);
39                     self.compute_aggregates();
40                 }
41             });
42
43             this.records.bind('remove', function () {
44                 if (self.editor.is_editing()) {
45                     self.cancel_edition();
46                 }
47             });
48
49             this.on('edit:before', this, function (event) {
50                 if (!self.editable() || self.editor.is_editing()) {
51                     event.cancel = true;
52                 }
53             });
54             this.on('edit:after', this, function () {
55                 self.$el.add(self.$buttons).addClass('oe_editing');
56                 self.$('.ui-sortable').sortable('disable');
57             });
58             this.on('save:after cancel:after', this, function () {
59                 self.$('.ui-sortable').sortable('enable');
60                 self.$el.add(self.$buttons).removeClass('oe_editing');
61             });
62         },
63         destroy: function () {
64             instance.web.bus.off('resize', this, this.resize_fields);
65             this._super();
66         },
67         do_hide: function () {
68             if (this.editor.is_editing()) {
69                 this.cancel_edition(true);
70             }
71             this._super();
72         },
73         sort_by_column: function (e) {
74             e.stopPropagation();
75             if (!this.editor.is_editing()) {
76                 this._super.apply(this, arguments);
77             }
78         },
79         /**
80          * Handles the activation of a record in editable mode (making a record
81          * editable), called *after* the record has become editable.
82          *
83          * The default behavior is to setup the listview's dataset to match
84          * whatever dataset was provided by the editing List
85          *
86          * @param {Number} index index of the record in the dataset
87          * @param {Object} id identifier of the record being edited
88          * @param {instance.web.DataSet} dataset dataset in which the record is available
89          */
90         do_edit: function (index, id, dataset) {
91             _.extend(this.dataset, dataset);
92         },
93         do_delete: function (ids) {
94             var nonfalse = _.compact(ids);
95             var _super = this._super.bind(this);
96             var next = this.editor.is_editing()
97                     ? this.cancel_edition(true)
98                     : $.when();
99             return next.then(function () {
100                 return _super(nonfalse);
101             });
102         },
103         editable: function () {
104             return !this.grouped
105                 && !this.options.disable_editable_mode
106                 && (this.fields_view.arch.attrs.editable
107                 || this._context_editable
108                 || this.options.editable);
109         },
110         /**
111          * Replace do_search to handle editability process
112          */
113         do_search: function(domain, context, group_by) {
114             var self=this, _super = self._super, args=arguments;
115             var ready = this.editor.is_editing()
116                     ? this.cancel_edition(true)
117                     : $.when();
118
119             return ready.then(function () {
120                 self._context_editable = !!context.set_editable;
121                 return _super.apply(self, args);
122             });
123         },
124         /**
125          * Replace do_add_record to handle editability (and adding new record
126          * as an editable row at the top or bottom of the list)
127          */
128         do_add_record: function () {
129             var self = this;
130             if (this.editable()) {
131                 this.$el.find('table:first').show();
132                 this.$el.find('.oe_view_nocontent').remove();
133                 this.start_edition();
134             } else {
135                 this._super();
136             }
137         },
138         load_list: function (data, grouped) {
139             var self = this;
140             // tree/@editable takes priority on everything else if present.
141             var result = this._super(data, grouped);
142
143             // In case current editor was started previously, also has to run
144             // when toggling from editable to non-editable in case form widgets
145             // have setup global behaviors expecting themselves to exist
146             // somehow.
147             this.editor.destroy();
148             // Editor is not restartable due to formview not being restartable
149             this.editor = this.make_editor();
150
151             if (this.editable()) {
152                 this.$el.addClass('oe_list_editable');
153                 // FIXME: any hook available to ensure this is only done once?
154                 this.$buttons
155                     .off('click', '.oe_list_save')
156                     .on('click', '.oe_list_save', this.proxy('save_edition'))
157                     .off('click', '.oe_list_discard')
158                     .on('click', '.oe_list_discard', function (e) {
159                         e.preventDefault();
160                         self.cancel_edition();
161                     });
162                 var editor_ready = this.editor.prependTo(this.$el)
163                     .done(this.proxy('setup_events'));
164
165                 return $.when(result, editor_ready);
166             } else {
167                 this.$el.removeClass('oe_list_editable');
168             }
169
170             return result;
171         },
172         /**
173          * Builds a new editor object
174          *
175          * @return {instance.web.list.Editor}
176          */
177         make_editor: function () {
178             return new instance.web.list.Editor(this);
179         },
180         do_button_action: function (name, id, callback) {
181             var self = this, args = arguments;
182             this.ensure_saved().done(function (done) {
183                 if (!id && done.created) {
184                     id = done.record.get('id');
185                 }
186                 self.handle_button(name, id, callback);
187             });
188         },
189         /**
190          * Ensures the editable list is saved (saves any pending edition if
191          * needed, or tries to)
192          *
193          * Returns a deferred to the end of the saving.
194          *
195          * @returns {$.Deferred}
196          */
197         ensure_saved: function () {
198             return this.save_edition();
199         },
200         /**
201          * Builds a record with the provided id (``false`` for a creation),
202          * setting all columns with ``false`` value so code which relies on
203          * having an actual value behaves correctly
204          *
205          * @param {*} id
206          * @return {instance.web.list.Record}
207          */
208         make_empty_record: function (id) {
209             var attrs = {id: id};
210             _(this.columns).chain()
211                 .filter(function (x) { return x.tag === 'field';})
212                 .pluck('name')
213                 .each(function (field) { attrs[field] = false; });
214             return new instance.web.list.Record(attrs);
215         },
216         /**
217          * Set up the edition of a record of the list view "inline"
218          *
219          * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
220          * @param {Object} [options]
221          * @param {String} [options.focus_field] field to focus at start of edition
222          * @return {jQuery.Deferred}
223          */
224         start_edition: function (record, options) {
225             var self = this;
226             var item = false;
227             if (record) {
228                 item = record.attributes;
229             } else {
230                 record = this.make_empty_record(false);
231                 this.records.add(record, {
232                     at: this.prepends_on_create() ? 0 : null});
233             }
234
235             return this.ensure_saved().then(function () {
236                 var $recordRow = self.groups.get_row_for(record);
237                 var cells = self.get_cells_for($recordRow);
238                 var fields = {};
239                 self.fields_for_resize.splice(0, self.fields_for_resize.length);
240                 return self.with_event('edit', {
241                     record: record.attributes,
242                     cancel: false
243                 }, function () {
244                     return self.editor.edit(item, function (field_name, field) {
245                         var cell = cells[field_name];
246                         if (!cell) {
247                             return;
248                         }
249
250                         // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
251                         field.$el.attr('data-fieldname', field_name);
252                         fields[field_name] = field;
253                         self.fields_for_resize.push({field: field, cell: cell});
254                     }, options).then(function () {
255                         $recordRow.addClass('oe_edition');
256                         self.resize_fields();
257                         var focus_field = options && options.focus_field ? options.focus_field : undefined;
258                         if (!focus_field){
259                             focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); });
260                         }
261                         if (focus_field) fields[focus_field].$el.find('input').select();
262                         return record.attributes;
263                     });
264                 }).fail(function () {
265                     // if the start_edition event is cancelled and it was a
266                     // creation, remove the newly-created empty record
267                     if (!record.get('id')) {
268                         self.records.remove(record);
269                     }
270                 });
271             });
272         },
273         get_cells_for: function ($row) {
274             var cells = {};
275             $row.children('td').each(function (index, el) {
276                 cells[el.getAttribute('data-field')] = el;
277             });
278             return cells;
279         },
280         /**
281          * If currently editing a row, resizes all registered form fields based
282          * on the corresponding row cell
283          */
284         resize_fields: function () {
285             if (!this.editor.is_editing()) { return; }
286             for(var i=0, len=this.fields_for_resize.length; i<len; ++i) {
287                 var item = this.fields_for_resize[i];
288                 if (!item.field.get('effective_invisible')) {
289                     this.resize_field(item.field, item.cell);
290                 }
291             }
292         },
293         /**
294          * Resizes a field's root element based on the corresponding cell of
295          * a listview row
296          *
297          * @param {instance.web.form.AbstractField} field
298          * @param {jQuery} cell
299          */
300         resize_field: function (field, cell) {
301             var $cell = $(cell);
302
303             field.set_dimensions($cell.outerHeight(), $cell.outerWidth());
304             field.$el.position({
305                 my: 'left top',
306                 at: 'left top',
307                 of: $cell
308             });
309         },
310         /**
311          * @return {jQuery.Deferred}
312          */
313         save_edition: function () {
314             var self = this;
315             return self.saving_mutex.exec(function() {
316                 if (!self.editor.is_editing()) {
317                     return $.when();
318                 }
319                 return self.with_event('save', {
320                     editor: self.editor,
321                     form: self.editor.form,
322                     cancel: false
323                 }, function () {
324                     return self.editor.save().then(function (attrs) {
325                         var created = false;
326                         var record = self.records.get(attrs.id);
327                         if (!record) {
328                             // new record
329                             created = true;
330                             record = self.records.find(function (r) {
331                                 return !r.get('id');
332                             }).set('id', attrs.id);
333                         }
334                         // onwrite callback could be altering & reloading the
335                         // record which has *just* been saved, so first perform all
336                         // onwrites then do a final reload of the record
337                         return self.handle_onwrite(record)
338                             .then(function () {
339                                 return self.reload_record(record); })
340                             .then(function () {
341                                 return { created: created, record: record }; });
342                     });
343                 });
344             });
345         },
346         /**
347          * @param {Boolean} [force=false] discards the data even if the form has been edited
348          * @return {jQuery.Deferred}
349          */
350         cancel_edition: function (force) {
351             var self = this;
352             return this.with_event('cancel', {
353                 editor: this.editor,
354                 form: this.editor.form,
355                 cancel: false
356             }, function () {
357                 return this.editor.cancel(force).then(function (attrs) {
358                     if (attrs.id) {
359                         var record = self.records.get(attrs.id);
360                         if (!record) {
361                             // Record removed by third party during edition
362                             return;
363                         }
364                         return self.reload_record(record);
365                     }
366                     var to_delete = self.records.find(function (r) {
367                         return !r.get('id');
368                     });
369                     if (to_delete) {
370                         self.records.remove(to_delete);
371                     }
372                 });
373             });
374         },
375         /**
376          * Executes an action on the view's editor bracketed by a cancellable
377          * event of the name provided.
378          *
379          * The event name provided will be post-fixed with ``:before`` and
380          * ``:after``, the ``event`` parameter will be passed alongside the
381          * ``:before`` variant and if the parameter's ``cancel`` key is set to
382          * ``true`` the action *will not be called* and the method will return
383          * a rejection
384          *
385          * @param {String} event_name name of the event
386          * @param {Object} event event object, provided to ``:before`` sub-event
387          * @param {Function} action callable, called with the view's editor as its context
388          * @param {Array} [args] supplementary arguments provided to the action
389          * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
390          * @return {jQuery.Deferred}
391          */
392         with_event: function (event_name, event, action) {
393             var self = this;
394             event = event || {};
395             this.trigger(event_name + ':before', event);
396             if (event.cancel) {
397                 return $.Deferred().reject({
398                     message: _.str.sprintf("Event %s:before cancelled",
399                                            event_name)});
400             }
401             return $.when(action.call(this)).done(function () {
402                 self.trigger.apply(self, [event_name + ':after']
403                         .concat(_.toArray(arguments)));
404             });
405         },
406         edition_view: function (editor) {
407             var view = $.extend(true, {}, this.fields_view);
408             view.arch.tag = 'form';
409             _.extend(view.arch.attrs, {
410                 'class': 'oe_form_container',
411                 version: '7.0'
412             });
413             _(view.arch.children).chain()
414                 .zip(_(this.columns).filter(function (c) {
415                     return !(c instanceof instance.web.list.MetaColumn);}))
416                 .each(function (ar) {
417                     var widget = ar[0], column = ar[1];
418                     var modifiers = _.extend({}, column.modifiers);
419                     widget.attrs.nolabel = true;
420                     if (modifiers['tree_invisible'] || widget.tag === 'button') {
421                         modifiers.invisible = true;
422                     }
423                     widget.attrs.modifiers = JSON.stringify(modifiers);
424                 });
425             return view;
426         },
427         handle_onwrite: function (source_record) {
428             var self = this;
429             var on_write_callback = self.fields_view.arch.attrs.on_write;
430             if (!on_write_callback) { return $.when(); }
431             return this.dataset.call(on_write_callback, [source_record.get('id')])
432                 .then(function (ids) {
433                     return $.when.apply(
434                         null, _(ids).map(
435                             _.bind(self.handle_onwrite_record, self, source_record)));
436                 });
437         },
438         handle_onwrite_record: function (source_record, id) {
439             var record = this.records.get(id);
440             if (!record) {
441                 // insert after the source record
442                 var index = this.records.indexOf(source_record) + 1;
443                 record = this.make_empty_record(id);
444                 this.records.add(record, {at: index});
445             }
446             return this.reload_record(record);
447         },
448         prepends_on_create: function () {
449             return this.editable() === 'top';
450         },
451         setup_events: function () {
452             var self = this;
453             _.each(this.editor.form.fields, function(field, field_name) {
454                 var set_invisible = function() {
455                     field.set({'force_invisible': field.get('effective_readonly')});
456                 };
457                 field.on("change:effective_readonly", self, set_invisible);
458                 set_invisible();
459                 field.on('change:effective_invisible', self, function () {
460                     if (field.get('effective_invisible')) { return; }
461                     var item = _(self.fields_for_resize).find(function (item) {
462                         return item.field === field;
463                     });
464                     if (item) {
465                         setTimeout(function() {
466                             self.resize_field(item.field, item.cell);
467                         }, 0);
468                     }
469                      
470                 });
471             });
472
473             this.editor.$el.on('keyup keypress keydown', function (e) {
474                 if (!self.editor.is_editing()) { return true; }
475                 var key = _($.ui.keyCode).chain()
476                     .map(function (v, k) { return {name: k, code: v}; })
477                     .find(function (o) { return o.code === e.which; })
478                     .value();
479                 if (!key) { return true; }
480                 var method = e.type + '_' + key.name;
481                 if (!(method in self)) { return true; }
482                 return self[method](e);
483             });
484         },
485         /**
486          * Saves the current record, and goes to the next one (creation or
487          * edition)
488          *
489          * @private
490          * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
491          * @param {Object} [options]
492          * @param {String} [options.focus_field]
493          * @return {*}
494          */
495         _next: function (next_record, options) {
496             next_record = next_record || 'succ';
497             var self = this;
498             return this.save_edition().then(function (saveInfo) {
499                 if (!saveInfo) { return null; }
500                 if (saveInfo.created) {
501                     return self.start_edition();
502                 }
503                 var record = self.records[next_record](
504                         saveInfo.record, {wraparound: true});
505                 return self.start_edition(record, options);
506             });
507         },
508         keypress_ENTER: function () {
509             return this._next();
510         },
511         keydown_ESCAPE: function (e) {
512             return false;
513         },
514         keyup_ESCAPE: function (e) {
515             return this.cancel_edition();
516         },
517         /**
518          * Gets the selection range (start, end) for the provided element,
519          * returns ``null`` if it can't get a range.
520          *
521          * @private
522          */
523         _text_selection_range: function (el) {
524             var selectionStart;
525             try {
526                 selectionStart = el.selectionStart;
527             } catch (e) {
528                 // radio or checkbox throw on selectionStart access
529                 return null;
530             }
531             if (selectionStart !== undefined) {
532                 return {
533                     start: selectionStart,
534                     end: el.selectionEnd
535                 };
536             } else if (document.body.createTextRange) {
537                 throw new Error("Implement text range handling for MSIE");
538             }
539             // Element without selection ranges (select, div/@contenteditable)
540             return null;
541         },
542         _text_cursor: function (el) {
543             var selection = this._text_selection_range(el);
544             if (!selection) {
545                 return null;
546             }
547             if (selection.start !== selection.end) {
548                 return {position: null, collapsed: false};
549             }
550             return {position: selection.start, collapsed: true};
551         },
552         /**
553          * Checks if the cursor is at the start of the provided el
554          *
555          * @param {HTMLInputElement | HTMLTextAreaElement}
556          * @returns {Boolean}
557          * @private
558          */
559         _at_start: function (cursor, el) {
560             return cursor.collapsed && (cursor.position === 0);
561         },
562         /**
563          * Checks if the cursor is at the end of the provided el
564          *
565          * @param {HTMLInputElement | HTMLTextAreaElement}
566          * @returns {Boolean}
567          * @private
568          */
569         _at_end: function (cursor, el) {
570             return cursor.collapsed && (cursor.position === el.value.length);
571         },
572         /**
573          * @param DOMEvent event
574          * @param {String} record_direction direction to move into to get the next record (pred | succ)
575          * @param {Function} is_valid_move whether the edition should be moved to the next record
576          * @private
577          */
578         _key_move_record: function (event, record_direction, is_valid_move) {
579             if (!this.editor.is_editing('edit')) { return $.when(); }
580             var cursor = this._text_cursor(event.target);
581             // if text-based input (has a cursor)
582             //    and selecting (not collapsed) or not at a field boundary
583             //        don't move to the next record
584             if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }
585
586             event.preventDefault();
587             var source_field = $(event.target).closest('[data-fieldname]')
588                     .attr('data-fieldname');
589             return this._next(record_direction, {focus_field: source_field});
590
591         },
592         keydown_UP: function (e) {
593             var self = this;
594             return this._key_move_record(e, 'pred', function (el, cursor) {
595                 return self._at_start(cursor, el);
596             });
597         },
598         keydown_DOWN: function (e) {
599             var self = this;
600             return this._key_move_record(e, 'succ', function (el, cursor) {
601                 return self._at_end(cursor, el);
602             });
603         },
604
605         keydown_LEFT: function (e) {
606             // If the cursor is at the beginning of the field
607             var source_field = $(e.target).closest('[data-fieldname]')
608                     .attr('data-fieldname');
609             var cursor = this._text_cursor(e.target);
610             if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }
611
612             var fields_order = this.editor.form.fields_order;
613             var field_index = _(fields_order).indexOf(source_field);
614
615             // Look for the closest visible form field to the left
616             var fields = this.editor.form.fields;
617             var field;
618             do {
619                 if (--field_index < 0) { return $.when(); }
620
621                 field = fields[fields_order[field_index]];
622             } while (!field.$el.is(':visible'));
623
624             // and focus it
625             field.focus();
626             return $.when();
627         },
628         keydown_RIGHT: function (e) {
629             // same as above, but with cursor at the end of the field and
630             // looking for new fields at the right
631             var source_field = $(e.target).closest('[data-fieldname]')
632                     .attr('data-fieldname');
633             var cursor = this._text_cursor(e.target);
634             if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }
635
636             var fields_order = this.editor.form.fields_order;
637             var field_index = _(fields_order).indexOf(source_field);
638
639             var fields = this.editor.form.fields;
640             var field;
641             do {
642                 if (++field_index >= fields_order.length) { return $.when(); }
643
644                 field = fields[fields_order[field_index]];
645             } while (!field.$el.is(':visible'));
646
647             field.focus();
648             return $.when();
649         },
650         keydown_TAB: function (e) {
651             var form = this.editor.form;
652             var last_field = _(form.fields_order).chain()
653                 .map(function (name) { return form.fields[name]; })
654                 .filter(function (field) { return field.$el.is(':visible'); })
655                 .last()
656                 .value();
657             // tabbed from last field in form
658             if (last_field && last_field.$el.has(e.target).length) {
659                 e.preventDefault();
660                 return this._next();
661             }
662             return $.when();
663         }
664     });
665
666     instance.web.list.Editor = instance.web.Widget.extend({
667         /**
668          * @constructs instance.web.list.Editor
669          * @extends instance.web.Widget
670          *
671          * Adapter between listview and formview for editable-listview purposes
672          *
673          * @param {instance.web.Widget} parent
674          * @param {Object} options
675          * @param {instance.web.FormView} [options.formView=instance.web.FormView]
676          * @param {Object} [options.delegate]
677          */
678         init: function (parent, options) {
679             this._super(parent);
680             this.options = options || {};
681             _.defaults(this.options, {
682                 formView: instance.web.FormView,
683                 delegate: this.getParent()
684             });
685             this.delegate = this.options.delegate;
686
687             this.record = null;
688
689             this.form = new (this.options.formView)(
690                 this, this.delegate.dataset, false, {
691                     initial_mode: 'edit',
692                     disable_autofocus: true,
693                     $buttons: $(),
694                     $pager: $()
695             });
696         },
697         start: function () {
698             var self = this;
699             var _super = this._super();            
700             this.form.embedded_view = this._validate_view(
701                     this.delegate.edition_view(this));
702             var form_ready = this.form.appendTo(this.$el).done(
703                 self.form.proxy('do_hide'));
704             return $.when(_super, form_ready);
705         },
706         _validate_view: function (edition_view) {
707             if (!edition_view) {
708                 throw new Error("editor delegate's #edition_view must return "
709                               + "a view descriptor");
710             }
711             var arch = edition_view.arch;
712             if (!(arch && arch.children instanceof Array)) {
713                 throw new Error("Editor delegate's #edition_view must have a" +
714                                 " non-empty arch");
715             }
716             if (arch.tag !== "form") {
717                 throw new Error("Editor delegate's #edition_view must have a" +
718                                 " 'form' root node");
719             }
720             if (!(arch.attrs && arch.attrs.version === "7.0")) {
721                 throw new Error("Editor delegate's #edition_view must be a" +
722                                 " version 7 view");
723             }
724             if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
725                 throw new Error("Editor delegate's #edition_view must have the" +
726                                 " class 'oe_form_container' on its root" +
727                                 " element");
728             }
729
730             return edition_view;
731         },
732
733         /**
734          *
735          * @param {String} [state] either ``new`` or ``edit``
736          * @return {Boolean}
737          */
738         is_editing: function (state) {
739             if (!this.record) {
740                 return false;
741             }
742             switch(state) {
743             case null: case undefined:
744                 return true;
745             case 'new': return !this.record.id;
746             case 'edit': return !!this.record.id;
747             }
748             throw new Error("is_editing's state filter must be either `new` or" +
749                             " `edit` if provided");
750         },
751         edit: function (record, configureField, options) {
752             // TODO: specify sequence of edit calls
753             var self = this;
754             var form = self.form;
755             var loaded = record
756                 ? form.trigger('load_record', _.extend({}, record))
757                 : form.load_defaults();
758             return $.when(loaded).then(function () {
759                 return form.do_show({reload: false});
760             }).then(function () {
761                 self.record = form.datarecord;
762                 _(form.fields).each(function (field, name) {
763                     configureField(name, field);
764                 });
765                 return form;
766             });
767         },
768         save: function () {
769             var self = this;
770             return this.form
771                 .save(this.delegate.prepends_on_create())
772                 .then(function (result) {
773                     var created = result.created && !self.record.id;
774                     if (created) {
775                         self.record.id = result.result;
776                     }
777                     return self.cancel();
778                 });
779         },
780         cancel: function (force) {
781             if (!(force || this.form.can_be_discarded())) {
782                 return $.Deferred().reject({
783                     message: _t("The form's data can not be discarded")}).promise();
784             }
785             var record = this.record;
786             this.record = null;
787             this.form.do_hide();
788             return $.when(record);
789         }
790     });
791
792     instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
793         passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved",
794         get_row_for: function (record) {
795             return _(this.children).chain()
796                 .invoke('get_row_for', record)
797                 .compact()
798                 .first()
799                 .value();
800         }
801     });
802
803     instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{
804         row_clicked: function (event) {
805             if (!this.view.editable() || ! this.view.is_action_enabled('edit')) {
806                 return this._super.apply(this, arguments);
807             }
808             var record_id = $(event.currentTarget).data('id');
809             return this.view.start_edition(
810                 record_id ? this.records.get(record_id) : null, {
811                 focus_field: $(event.target).data('field')
812             });
813         },
814         /**
815          * If a row mapping to the record (@data-id matching the record's id or
816          * no @data-id if the record has no id), returns it. Otherwise returns
817          * ``null``.
818          *
819          * @param {Record} record the record to get a row for
820          * @return {jQuery|null}
821          */
822         get_row_for: function (record) {
823             var id;
824             var $row = this.$current.children('[data-id=' + record.get('id') + ']');
825             if ($row.length) {
826                 return $row;
827             }
828             return null;
829         }
830     });
831 })();