[FIX] forgot to forward arguments correctly in xmo@openerp.com-20120112084910-6fxbzbg...
[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 openerp.web.list_editable = function (openerp) {
6     var KEY_RETURN = 13,
7         KEY_ESCAPE = 27;
8     var QWeb = openerp.web.qweb;
9
10     // editability status of list rows
11     openerp.web.ListView.prototype.defaults.editable = null;
12
13     // TODO: not sure second @lends on existing item is correct, to check
14     openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
15         init: function () {
16             var self = this;
17             this._super.apply(this, arguments);
18             $(this.groups).bind({
19                 'edit': function (e, id, dataset) {
20                     self.do_edit(dataset.index, id, dataset);
21                 },
22                 'saved': function () {
23                     if (self.groups.get_selection().length) {
24                         return;
25                     }
26                     self.configure_pager(self.dataset);
27                     self.compute_aggregates();
28                 }
29             })
30         },
31         /**
32          * Handles the activation of a record in editable mode (making a record
33          * editable), called *after* the record has become editable.
34          *
35          * The default behavior is to setup the listview's dataset to match
36          * whatever dataset was provided by the editing List
37          *
38          * @param {Number} index index of the record in the dataset
39          * @param {Object} id identifier of the record being edited
40          * @param {openerp.web.DataSet} dataset dataset in which the record is available
41          */
42         do_edit: function (index, id, dataset) {
43             _.extend(this.dataset, dataset);
44         },
45         /**
46          * Sets editability status for the list, based on defaults, view
47          * architecture and the provided flag, if any.
48          *
49          * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
50          */
51         set_editable: function (force) {
52             // If ``force``, set editability to bottom
53             // otherwise rely on view default
54             // view' @editable is handled separately as we have not yet
55             // fetched and processed the view at this point.
56             this.options.editable = (
57                     (force && "bottom")
58                     || this.defaults.editable);
59         },
60         /**
61          * Replace do_search to handle editability process
62          */
63         do_search: function(domain, context, group_by) {
64             this.set_editable(context['set_editable']);
65             this._super.apply(this, arguments);
66         },
67         /**
68          * Replace do_add_record to handle editability (and adding new record
69          * as an editable row at the top or bottom of the list)
70          */
71         do_add_record: function () {
72             if (this.options.editable) {
73                 this.groups.new_record();
74             } else {
75                 this._super();
76             }
77         },
78         on_loaded: function (data, grouped) {
79             // tree/@editable takes priority on everything else if present.
80             this.options.editable = data.arch.attrs.editable || this.options.editable;
81             return this._super(data, grouped);
82         },
83         /**
84          * Ensures the editable list is saved (saves any pending edition if
85          * needed, or tries to)
86          *
87          * Returns a deferred to the end of the saving.
88          *
89          * @returns {$.Deferred}
90          */
91         ensure_saved: function () {
92             return this.groups.ensure_saved();
93         }
94     });
95
96     openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
97         passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
98         new_record: function () {
99             // TODO: handle multiple children
100             this.children[null].new_record();
101         },
102         /**
103          * Ensures descendant editable List instances are all saved if they have
104          * pending editions.
105          *
106          * @returns {$.Deferred}
107          */
108         ensure_saved: function () {
109             return $.when.apply(null,
110                 _.invoke(
111                     _.values(this.children),
112                     'ensure_saved'));
113         }
114     });
115
116     openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
117         row_clicked: function (event) {
118             if (!this.options.editable) {
119                 return this._super.apply(this, arguments);
120             }
121             this.edit_record($(event.currentTarget).data('id'));
122         },
123         /**
124          * Checks if a record is being edited, and if so cancels it
125          */
126         cancel_pending_edition: function () {
127             var self = this, cancelled = $.Deferred();
128             if (!this.edition) {
129                 cancelled.resolve();
130                 return cancelled.promise();
131             }
132
133             if (this.edition_id != null) {
134                 this.reload_record(self.records.get(this.edition_id)).then(function () {
135                     cancelled.resolve();
136                 });
137             } else {
138                 cancelled.resolve();
139             }
140             cancelled.then(function () {
141                 self.view.unpad_columns();
142                 self.edition_form.stop();
143                 self.edition_form.$element.remove();
144                 delete self.edition_form;
145                 delete self.edition_id;
146                 delete self.edition;
147             });
148             this.pad_table_to(5);
149             return cancelled.promise();
150         },
151         /**
152          * Adapts this list's view description to be suitable to the inner form
153          * view of a row being edited.
154          *
155          * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
156          */
157         get_form_fields_view: function () {
158             // deep copy of view
159             var view = $.extend(true, {}, this.group.view.fields_view);
160             _(view.arch.children).each(function (widget) {
161                 widget.attrs.nolabel = true;
162                 if (widget.tag === 'button') {
163                     delete widget.attrs.string;
164                 }
165             });
166             view.arch.attrs.col = 2 * view.arch.children.length;
167             return view;
168         },
169         on_row_keyup: function (e) {
170             var self = this;
171             switch (e.which) {
172             case KEY_RETURN:
173                 this.save_row().then(function (result) {
174                     if (result.created) {
175                         self.new_record();
176                         return;
177                     }
178
179                     var next_record_id,
180                         next_record = self.records.at(
181                                 self.records.indexOf(result.edited_record) + 1);
182                     if (next_record) {
183                         next_record_id = next_record.get('id');
184                         self.dataset.index = _(self.dataset.ids)
185                                 .indexOf(next_record_id);
186                     } else {
187                         self.dataset.index = 0;
188                         next_record_id = self.records.at(0).get('id');
189                     }
190                     self.edit_record(next_record_id);
191                 });
192                 break;
193             case KEY_ESCAPE:
194                 this.cancel_edition();
195                 break;
196             }
197         },
198         render_row_as_form: function (row) {
199             var self = this;
200             this.cancel_pending_edition().then(function () {
201                 var record_id = $(row).data('id');
202                 var $new_row = $('<tr>', {
203                         id: _.uniqueId('oe-editable-row-'),
204                         'data-id': record_id,
205                         'class': row ? $(row).attr('class') : '' + ' oe_forms',
206                         click: function (e) {e.stopPropagation();}
207                     })
208                     .delegate('button.oe-edit-row-save', 'click', function () {
209                         self.save_row();
210                     })
211                     .delegate('button', 'keyup', function (e) {
212                         e.stopImmediatePropagation();
213                     })
214                     .keyup(function () {
215                         return self.on_row_keyup.apply(self, arguments); });
216                 if (row) {
217                     $new_row.replaceAll(row);
218                 } else if (self.options.editable) {
219                     var $last_child = self.$current.children('tr:last');
220                     if (self.records.length) {
221                         if (self.options.editable === 'top') {
222                             $new_row.insertBefore(
223                                 self.$current.children('[data-id]:first'));
224                         } else {
225                             $new_row.insertAfter(
226                                 self.$current.children('[data-id]:last'));
227                         }
228                     } else {
229                         $new_row.prependTo(self.$current);
230                     }
231                     if ($last_child.is(':not([data-id])')) {
232                         $last_child.remove();
233                     }
234                 }
235                 self.edition = true;
236                 self.edition_id = record_id;
237                 self.edition_form = _.extend(new openerp.web.ListEditableFormView(self.view, self.dataset, false), {
238                     form_template: 'ListView.row.form',
239                     registry: openerp.web.list.form.widgets,
240                     $element: $new_row
241                 });
242                 // HA HA
243                 self.edition_form.appendTo();
244                 $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
245                     // put in $.when just in case  FormView.on_loaded becomes asynchronous
246                     $new_row.find('> td')
247                           .addClass('oe-field-cell')
248                           .removeAttr('width')
249                       .end()
250                       .find('td:last').removeClass('oe-field-cell').end();
251                     if (self.options.selectable) {
252                         $new_row.prepend('<th>');
253                     }
254                     if (self.options.isClarkGable) {
255                         $new_row.prepend('<th>');
256                     }
257                     // pad in case of groupby
258                     _(self.columns).each(function (column) {
259                         if (column.meta) {
260                             $new_row.prepend('<td>');
261                         }
262                     });
263                     // Add column for the save, if
264                     // there is none in the list
265                     if (!self.options.deletable) {
266                         self.view.pad_columns(
267                             1, {except: $new_row});
268                     }
269
270                     self.edition_form.do_show();
271                 });
272             });
273         },
274         handle_onwrite: function (source_record_id) {
275             var self = this;
276             var on_write_callback = self.view.fields_view.arch.attrs.on_write;
277             if (!on_write_callback) { return; }
278             this.dataset.call(on_write_callback, [source_record_id], function (ids) {
279                 _(ids).each(function (id) {
280                     var record = self.records.get(id);
281                     if (!record) {
282                         // insert after the source record
283                         var index = self.records.indexOf(
284                             self.records.get(source_record_id)) + 1;
285                         record = new openerp.web.list.Record({id: id});
286                         self.records.add(record, {at: index});
287                         self.dataset.ids.splice(index, 0, id);
288                     }
289                     self.reload_record(record);
290                 });
291             });
292         },
293         /**
294          * Saves the current row, and returns a Deferred resolving to an object
295          * with the following properties:
296          *
297          * ``created``
298          *   Boolean flag indicating whether the record saved was being created
299          *   (``true`` or edited (``false``)
300          * ``edited_record``
301          *   The result of saving the record (either the newly created record,
302          *   or the post-edition record), after insertion in the Collection if
303          *   needs be.
304          *
305          * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
306          */
307         save_row: function () {
308             //noinspection JSPotentiallyInvalidConstructorUsage
309             var self = this, done = $.Deferred();
310             return this.edition_form
311                 .do_save(null, this.options.editable === 'top')
312                 .pipe(function (result) {
313                     if (result.created && !self.edition_id) {
314                         self.records.add({id: result.result},
315                             {at: self.options.editable === 'top' ? 0 : null});
316                         self.edition_id = result.result;
317                     }
318                     var edited_record = self.records.get(self.edition_id);
319
320                     return $.when(
321                         self.handle_onwrite(self.edition_id),
322                         self.cancel_pending_edition().then(function () {
323                             $(self).trigger('saved', [self.dataset]);
324                         })).pipe(function () {
325                             return {
326                                 created: result.created || false,
327                                 edited_record: edited_record
328                             };
329                         }, null);
330                 }, null);
331         },
332         /**
333          * If the current list is being edited, ensures it's saved
334          */
335         ensure_saved: function () {
336             if (this.edition) {
337                 return this.save_row();
338             }
339             //noinspection JSPotentiallyInvalidConstructorUsage
340             return $.Deferred().resolve().promise();
341         },
342         /**
343          * Cancels the edition of the row for the current dataset index
344          */
345         cancel_edition: function () {
346             this.cancel_pending_edition();
347         },
348         /**
349          * Edits record currently selected via dataset
350          */
351         edit_record: function (record_id) {
352             this.render_row_as_form(
353                 this.$current.find('[data-id=' + record_id + ']'));
354             $(this).trigger(
355                 'edit',
356                 [record_id, this.dataset]);
357         },
358         new_record: function () {
359             this.dataset.index = null;
360             this.render_row_as_form();
361         },
362         render_record: function (record) {
363             var index = this.records.indexOf(record),
364                  self = this;
365             // FIXME: context dict should probably be extracted cleanly
366             return QWeb.render('ListView.row', {
367                 columns: this.columns,
368                 options: this.options,
369                 record: record,
370                 row_parity: (index % 2 === 0) ? 'even' : 'odd',
371                 view: this.view,
372                 render_cell: function () {
373                     return self.render_cell.apply(self, arguments); },
374                 edited: !!this.edition_form
375             });
376         }
377     });
378     if (!openerp.web.list) {
379         openerp.web.list = {};
380     }
381     if (!openerp.web.list.form) {
382         openerp.web.list.form = {};
383     }
384     openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
385         template: 'ListView.row.frame'
386     });
387     var form_widgets = openerp.web.form.widgets;
388     openerp.web.list.form.widgets = form_widgets.clone({
389         'frame': 'openerp.web.list.form.WidgetFrame'
390     });
391     // All form widgets inherit a problematic behavior from
392     // openerp.web.form.WidgetFrame: the cell itself is removed when invisible
393     // whether it's @invisible or @attrs[invisible]. In list view, only the
394     // former should completely remove the cell. We need to override update_dom
395     // on all widgets since we can't just hit on widget itself (I think)
396     var list_form_widgets = openerp.web.list.form.widgets;
397     _(list_form_widgets.map).each(function (widget_path, key) {
398         if (key === 'frame') { return; }
399         var new_path = 'openerp.web.list.form.' + key;
400
401         openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
402             update_dom: function () {
403                 this.$element.children().css('visibility', '');
404                 if (this.modifiers.tree_invisible) {
405                     var old_invisible = this.invisible;
406                     this.invisible = true;
407                     this._super();
408                     this.invisible = old_invisible;
409                 } else if (this.invisible) {
410                     this.$element.children().css('visibility', 'hidden');
411                 } else {
412                     this._super();
413                 }
414             }
415         });
416         list_form_widgets.add(key, new_path);
417     });
418     
419     openerp.web.ListEditableFormView = openerp.web.FormView.extend({
420         init_view: function() {},
421         _render_and_insert: function () {
422             return this.start();
423         }
424     });
425 };