cf48e736d6d6e645c5dac3fe2c6f0fa18eeac8ee
[odoo/odoo.git] / addons / base / static / src / js / list-editable.js
1 /**
2  * @namespace handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
3  */
4 openerp.base.list.editable = function (openerp) {
5     var KEY_RETURN = 13,
6         KEY_ESCAPE = 27;
7
8     // editability status of list rows
9     openerp.base.ListView.prototype.defaults.editable = null;
10
11     var old_init = openerp.base.ListView.prototype.init,
12         old_actual_search = openerp.base.ListView.prototype.do_actual_search,
13         old_add_record = openerp.base.ListView.prototype.do_add_record,
14         old_on_loaded = openerp.base.ListView.prototype.on_loaded;
15     // TODO: not sure second @lends on existing item is correct, to check
16     _.extend(openerp.base.ListView.prototype, /** @lends openerp.base.ListView# */{
17         init: function () {
18             var self = this;
19             old_init.apply(this, arguments);
20             $(this.groups).bind({
21                 'edit': function (e, id, dataset) {
22                     self.do_edit(dataset.index, id, dataset);
23                 },
24                 'saved': function () {
25                     if (self.groups.get_selection().length) {
26                         return;
27                     }
28                     self.compute_aggregates();
29                 }
30             })
31         },
32         /**
33          * Handles the activation of a record in editable mode (making a record
34          * editable), called *after* the record has become editable.
35          *
36          * The default behavior is to setup the listview's dataset to match
37          * whatever dataset was provided by the editing List
38          *
39          * @param {Number} index index of the record in the dataset
40          * @param {Object} id identifier of the record being edited
41          * @param {openerp.base.DataSet} dataset dataset in which the record is available
42          */
43         do_edit: function (index, id, dataset) {
44             _.extend(this.dataset, dataset);
45         },
46         /**
47          * Sets editability status for the list, based on defaults, view
48          * architecture and the provided flag, if any.
49          *
50          * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
51          */
52         set_editable: function (force) {
53             // If ``force``, set editability to bottom
54             // otherwise rely on view default
55             // view' @editable is handled separately as we have not yet
56             // fetched and processed the view at this point.
57             this.options.editable = (
58                     (force && "bottom")
59                     || this.defaults.editable);
60         },
61         /**
62          * Replace do_actual_search to handle editability process
63          */
64         do_actual_search: function (results) {
65             this.set_editable(results.context['set_editable']);
66             old_actual_search.call(this, results);
67         },
68         /**
69          * Replace do_add_record to handle editability (and adding new record
70          * as an editable row at the top or bottom of the list)
71          */
72         do_add_record: function () {
73             if (this.options.editable) {
74                 this.groups.new_record();
75             } else {
76                 old_add_record.call(this);
77             }
78         },
79         on_loaded: function (data, grouped) {
80             // tree/@editable takes priority on everything else if present.
81             this.options.editable = data.fields_view.arch.attrs.editable || this.options.editable;
82             return old_on_loaded.call(this, data, grouped);
83         }
84     });
85
86     _.extend(openerp.base.ListView.Groups.prototype, /** @lends openerp.base.ListView.Groups# */{
87         passtrough_events: openerp.base.ListView.Groups.prototype.passtrough_events + " edit saved",
88         new_record: function () {
89             // TODO: handle multiple children
90             this.children[null].new_record();
91         }
92     });
93
94     var old_list_row_clicked = openerp.base.ListView.List.prototype.row_clicked;
95     _.extend(openerp.base.ListView.List.prototype, /** @lends openerp.base.ListView.List */{
96         row_clicked: function (event) {
97             if (!this.options.editable) {
98                 return old_list_row_clicked.call(this, event);
99             }
100             this.edit_record();
101         },
102         /**
103          * Checks if a record is being edited, and if so cancels it
104          */
105         cancel_pending_edition: function () {
106             var self = this, cancelled = $.Deferred();
107             if (!this.edition) {
108                 cancelled.resolve();
109                 return cancelled.promise();
110             }
111
112             if (this.edition_index !== null) {
113                 this.reload_record(this.edition_index, true).then(function () {
114                     cancelled.resolve();
115                 });
116             } else {
117                 cancelled.resolve();
118             }
119             cancelled.then(function () {
120                 self.edition_form.stop();
121                 self.edition_form.$element.remove();
122                 delete self.edition_form;
123                 delete self.edition_index;
124                 delete self.edition;
125             });
126             return cancelled.promise();
127         },
128         /**
129          * Adapts this list's view description to be suitable to the inner form view of a row being edited.
130          *
131          * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
132          */
133         get_form_fields_view: function () {
134             // deep copy of view
135             var view = $.extend(true, {}, this.group.view.fields_view);
136             _(view.arch.children).each(function (widget) {
137                 widget.attrs.nolabel = true;
138                 if (widget.tag === 'button') {
139                     delete widget.attrs.string;
140                 }
141             });
142             view.arch.attrs.col = 2 * view.arch.children.length;
143             return view;
144         },
145         render_row_as_form: function (row) {
146             var self = this;
147             this.cancel_pending_edition().then(function () {
148                 var $new_row = $('<tr>', {
149                         id: _.uniqueId('oe-editable-row-'),
150                         'class': $(row).attr('class') + ' oe_forms',
151                         click: function (e) {e.stopPropagation();}
152                     })
153                     .delegate('button.oe-edit-row-save', 'click', function () {
154                         self.save_row();
155                     })
156                     .delegate('button.oe-edit-row-cancel', 'click', function () {
157                         self.cancel_edition();
158                     })
159                     .delegate('button', 'keyup', function (e) {
160                         e.stopImmediatePropagation();
161                     })
162                     .keyup(function (e) {
163                         switch (e.which) {
164                             case KEY_RETURN:
165                                 self.save_row(true);
166                                 break;
167                             case KEY_ESCAPE:
168                                 self.cancel_edition();
169                                 break;
170                             default:
171                                 return;
172                         }
173                     });
174                 if (row) {
175                     $new_row.replaceAll(row);
176                 } else if (self.options.editable === 'top') {
177                     self.$current.prepend($new_row);
178                 } else if (self.options.editable) {
179                     self.$current.append($new_row);
180                 }
181                 self.edition = true;
182                 self.edition_index = self.dataset.index;
183                 self.edition_form = _.extend(new openerp.base.FormView(
184                         self, $new_row.attr('id'), self.dataset, false), {
185                     template: 'ListView.row.form',
186                     registry: openerp.base.list.form.widgets
187                 });
188                 $.when(self.edition_form.on_loaded({fields_view: self.get_form_fields_view()})).then(function () {
189                     // put in $.when just in case  FormView.on_loaded becomes asynchronous
190                     $new_row.find('td')
191                           .addClass('oe-field-cell')
192                           .removeAttr('width')
193                       .end()
194                       .find('td:first').removeClass('oe-field-cell').end()
195                       .find('td:last').removeClass('oe-field-cell').end();
196                     // pad in case of groupby
197                     _(self.columns).each(function (column) {
198                         if (column.meta) {
199                             $new_row.prepend('<td>');
200                         }
201                     });
202
203                     self.edition_form.do_show();
204                 });
205             });
206         },
207         /**
208          * Saves the current row, and triggers the edition of its following
209          * sibling if asked.
210          *
211          * @param {Boolean} [edit_next=false] should the next row become editable
212          */
213         save_row: function (edit_next) {
214             var self = this;
215             this.edition_form.do_save(function (result) {
216                 if (result.created && !self.edition_index) {
217                     self.edition_index = self.dataset.index;
218                 }
219                 self.cancel_pending_edition().then(function () {
220                     $(self).trigger('saved', [self.dataset]);
221                     if (!edit_next) {
222                         return;
223                     }
224                     if (result.created) {
225                         self.new_record();
226                         return;
227                     }
228                     self.dataset.next();
229                     self.edit_record();
230                 });
231             }, this.options.editable === 'top');
232         },
233         /**
234          * Cancels the edition of the row for the current dataset index
235          */
236         cancel_edition: function () {
237             this.cancel_pending_edition();
238         },
239         /**
240          * Edits record currently selected via dataset
241          */
242         edit_record: function () {
243             this.render_row_as_form(
244                 this.$current.children(
245                     _.sprintf('[data-index=%d]',
246                             this.dataset.index)));
247             $(this).trigger(
248                 'edit',
249                 [this.rows[this.dataset.index].data.id.value, this.dataset]);
250         },
251         new_record: function () {
252             this.dataset.index = null;
253             this.render_row_as_form();
254         }
255     });
256     if (!openerp.base.list) {
257         openerp.base.list = {};
258     }
259     if (!openerp.base.list.form) {
260         openerp.base.list.form = {};
261     }
262     openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({
263         template: 'ListView.row.frame'
264     });
265     var form_widgets = openerp.base.form.widgets;
266     openerp.base.list.form.widgets = form_widgets.clone({
267         'frame': 'openerp.base.list.form.WidgetFrame'
268     });
269     // All form widgets inherit a problematic behavior from
270     // openerp.base.form.WidgetFrame: the cell itself is removed when invisible
271     // whether it's @invisible or @attrs[invisible]. In list view, only the
272     // former should completely remove the cell. We need to override update_dom
273     // on all widgets since we can't just hit on widget itself (I think)
274     var list_form_widgets = openerp.base.list.form.widgets;
275     _(list_form_widgets.map).each(function (widget_path, key) {
276         if (key === 'frame') { return; }
277         var new_path = 'openerp.base.list.form.' + key;
278
279         openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({
280             update_dom: function () {
281                 this.$element.children().css('visibility', '');
282                 if (this.modifiers.tree_invisible) {
283                     var old_invisible = this.invisible;
284                     this.invisible = !!this.modifiers.tree_invisible;
285                     this._super();
286                     this.invisible = old_invisible;
287                 } else if (this.invisible) {
288                     this.$element.children().css('visibility', 'hidden');
289                 }
290             }
291         });
292         list_form_widgets.add(key, new_path);
293     });
294 };