[WIP] Breadcrumb
[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 (instance) {
6     var KEY_RETURN = 13,
7         KEY_ESCAPE = 27;
8     var QWeb = instance.web.qweb;
9
10     // editability status of list rows
11     instance.web.ListView.prototype.defaults.editable = null;
12
13     // TODO: not sure second @lends on existing item is correct, to check
14     instance.web.ListView.include(/** @lends instance.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 {instance.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                     ! this.options.read_only && ((force && "bottom") || this.defaults.editable));
58         },
59         /**
60          * Replace do_search to handle editability process
61          */
62         do_search: function(domain, context, group_by) {
63             this.set_editable(context['set_editable']);
64             this._super.apply(this, arguments);
65         },
66         /**
67          * Replace do_add_record to handle editability (and adding new record
68          * as an editable row at the top or bottom of the list)
69          */
70         do_add_record: function () {
71             if (this.options.editable) {
72                 this.$element.find('table:first').show();
73                 this.$element.find('.oe_view_nocontent').remove();
74                 this.groups.new_record();
75             } else {
76                 this._super();
77             }
78         },
79         on_loaded: function (data, grouped) {
80             // tree/@editable takes priority on everything else if present.
81             this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable);
82             return this._super(data, grouped);
83         },
84         /**
85          * Ensures the editable list is saved (saves any pending edition if
86          * needed, or tries to)
87          *
88          * Returns a deferred to the end of the saving.
89          *
90          * @returns {$.Deferred}
91          */
92         ensure_saved: function () {
93             return this.groups.ensure_saved();
94         }
95     });
96
97     instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
98         passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved",
99         new_record: function () {
100             // TODO: handle multiple children
101             this.children[null].new_record();
102         },
103         /**
104          * Ensures descendant editable List instances are all saved if they have
105          * pending editions.
106          *
107          * @returns {$.Deferred}
108          */
109         ensure_saved: function () {
110             return $.when.apply(null,
111                 _.invoke(
112                     _.values(this.children),
113                     'ensure_saved'));
114         }
115     });
116
117     instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{
118         row_clicked: function (event) {
119             if (!this.options.editable) {
120                 return this._super.apply(this, arguments);
121             }
122             this.edit_record($(event.currentTarget).data('id'));
123         },
124         /**
125          * Checks if a record is being edited, and if so cancels it
126          */
127         cancel_pending_edition: function () {
128             var self = this, cancelled;
129             if (!this.edition) {
130                 return $.when();
131             }
132
133             if (this.edition_id) {
134                 cancelled = this.reload_record(this.records.get(this.edition_id));
135             } else {
136                 cancelled = $.when();
137             }
138             cancelled.then(function () {
139                 self.view.unpad_columns();
140                 self.edition_form.destroy();
141                 self.edition_form.$element.remove();
142                 delete self.edition_form;
143                 self.dataset.index = null;
144                 delete self.edition_id;
145                 delete self.edition;
146             });
147             this.pad_table_to(5);
148             return cancelled;
149         },
150         /**
151          * Adapts this list's view description to be suitable to the inner form
152          * view of a row being edited.
153          *
154          * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
155          */
156         get_form_fields_view: function () {
157             // deep copy of view
158             var view = $.extend(true, {}, this.group.view.fields_view);
159             _(view.arch.children).each(function (widget) {
160                 widget.attrs.nolabel = true;
161                 if (widget.tag === 'button') {
162                     delete widget.attrs.string;
163                 }
164             });
165             view.arch.attrs.col = 2 * view.arch.children.length;
166             return view;
167         },
168         on_row_keyup: function (e) {
169             var self = this;
170             switch (e.which) {
171             case KEY_RETURN:
172                 $(e.target).blur();
173                 e.preventDefault();
174                 //e.stopImmediatePropagation();
175                 setTimeout(function () {
176                     self.save_row().then(function (result) {
177                         if (result.created) {
178                             self.new_record();
179                             return;
180                         }
181
182                         var next_record_id,
183                             next_record = self.records.at(
184                                     self.records.indexOf(result.edited_record) + 1);
185                         if (next_record) {
186                             next_record_id = next_record.get('id');
187                             self.dataset.index = _(self.dataset.ids)
188                                     .indexOf(next_record_id);
189                         } else {
190                             self.dataset.index = 0;
191                             next_record_id = self.records.at(0).get('id');
192                         }
193                         self.edit_record(next_record_id);
194                     }, 0);
195                 });
196                 break;
197             case KEY_ESCAPE:
198                 this.cancel_edition();
199                 break;
200             }
201         },
202         render_row_as_form: function (row) {
203             var self = this;
204             return this.ensure_saved().pipe(function () {
205                 var record_id = $(row).data('id');
206                 var $new_row = $('<tr>', {
207                         id: _.uniqueId('oe-editable-row-'),
208                         'data-id': record_id,
209                         'class': (row ? $(row).attr('class') : ''),
210                         click: function (e) {e.stopPropagation();}
211                     })
212                     .addClass('oe_form oe_form_container')
213                     .delegate('button.oe_list_edit_row_save', 'click', function () {
214                         self.save_row();
215                     })
216                     .delegate('button', 'keyup', function (e) {
217                         e.stopImmediatePropagation();
218                     })
219                     .keyup(function () {
220                         return self.on_row_keyup.apply(self, arguments); })
221                     .keydown(function (e) { e.stopPropagation(); })
222                     .keypress(function (e) {
223                         if (e.which === KEY_RETURN) {
224                             return false;
225                         }
226                     });
227
228                 if (row) {
229                     $new_row.replaceAll(row);
230                 } else if (self.options.editable) {
231                     var $last_child = self.$current.children('tr:last');
232                     if (self.records.length) {
233                         if (self.options.editable === 'top') {
234                             $new_row.insertBefore(
235                                 self.$current.children('[data-id]:first'));
236                         } else {
237                             $new_row.insertAfter(
238                                 self.$current.children('[data-id]:last'));
239                         }
240                     } else {
241                         $new_row.prependTo(self.$current);
242                     }
243                     if ($last_child.is(':not([data-id])')) {
244                         $last_child.remove();
245                     }
246                 }
247                 self.edition = true;
248                 self.edition_id = record_id;
249                 self.dataset.index = _(self.dataset.ids).indexOf(record_id);
250                 if (self.dataset.index === -1) {
251                     self.dataset.index = null;
252                 }
253                 self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), {
254                     $element: $new_row,
255                     editable_list: self
256                 });
257                 // put in $.when just in case  FormView.on_loaded becomes asynchronous
258                 return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
259                     $new_row.find('> td')
260                       .end()
261                       .find('td:last').removeClass('oe_list_field_cell').end();
262                     // pad in case of groupby
263                     _(self.columns).each(function (column) {
264                         if (column.meta) {
265                             $new_row.prepend('<td>');
266                         }
267                     });
268                     // Add column for the save, if
269                     // there is none in the list
270                     if (!self.options.deletable) {
271                         self.view.pad_columns(
272                             1, {except: $new_row});
273                     }
274
275                     self.edition_form.do_show();
276                 });
277             });
278         },
279         handle_onwrite: function (source_record_id) {
280             var self = this;
281             var on_write_callback = self.view.fields_view.arch.attrs.on_write;
282             if (!on_write_callback) { return; }
283             this.dataset.call(on_write_callback, [source_record_id], function (ids) {
284                 _(ids).each(function (id) {
285                     var record = self.records.get(id);
286                     if (!record) {
287                         // insert after the source record
288                         var index = self.records.indexOf(
289                             self.records.get(source_record_id)) + 1;
290                         record = new instance.web.list.Record({id: id});
291                         self.records.add(record, {at: index});
292                         self.dataset.ids.splice(index, 0, id);
293                     }
294                     self.reload_record(record);
295                 });
296             });
297         },
298         /**
299          * Saves the current row, and returns a Deferred resolving to an object
300          * with the following properties:
301          *
302          * ``created``
303          *   Boolean flag indicating whether the record saved was being created
304          *   (``true`` or edited (``false``)
305          * ``edited_record``
306          *   The result of saving the record (either the newly created record,
307          *   or the post-edition record), after insertion in the Collection if
308          *   needs be.
309          *
310          * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
311          */
312         save_row: function () {
313             //noinspection JSPotentiallyInvalidConstructorUsage
314             var self = this;
315             return this.edition_form
316                 .do_save(null, this.options.editable === 'top')
317                 .pipe(function (result) {
318                     if (result.created && !self.edition_id) {
319                         self.records.add({id: result.result},
320                             {at: self.options.editable === 'top' ? 0 : null});
321                         self.edition_id = result.result;
322                     }
323                     var edited_record = self.records.get(self.edition_id);
324
325                     return $.when(
326                         self.handle_onwrite(self.edition_id),
327                         self.cancel_pending_edition().then(function () {
328                             $(self).trigger('saved', [self.dataset]);
329                         })).pipe(function () {
330                             return {
331                                 created: result.created || false,
332                                 edited_record: edited_record
333                             };
334                         });
335                 });
336         },
337         /**
338          * If the current list is being edited, ensures it's saved
339          */
340         ensure_saved: function () {
341             if (this.edition) {
342                 // kinda-hack-ish: if the user has entered data in a field,
343                 // oe_form_dirty will be set on the form so save, otherwise
344                 // discard the current (entirely empty) line
345                 if (this.edition_form.$element.is('.oe_form_dirty')) {
346                     return this.save_row();
347                 }
348                 return this.cancel_pending_edition();
349             }
350             //noinspection JSPotentiallyInvalidConstructorUsage
351             return $.when();
352         },
353         /**
354          * Cancels the edition of the row for the current dataset index
355          */
356         cancel_edition: function () {
357             this.cancel_pending_edition();
358         },
359         /**
360          * Edits record currently selected via dataset
361          */
362         edit_record: function (record_id) {
363             this.render_row_as_form(
364                 this.$current.find('[data-id=' + record_id + ']'));
365             $(this).trigger(
366                 'edit',
367                 [record_id, this.dataset]);
368         },
369         new_record: function () {
370             this.render_row_as_form();
371         },
372         render_record: function (record) {
373             var index = this.records.indexOf(record),
374                  self = this;
375             // FIXME: context dict should probably be extracted cleanly
376             return QWeb.render('ListView.row', {
377                 columns: this.columns,
378                 options: this.options,
379                 record: record,
380                 row_parity: (index % 2 === 0) ? 'even' : 'odd',
381                 view: this.view,
382                 render_cell: function () {
383                     return self.render_cell.apply(self, arguments); },
384                 edited: !!this.edition_form
385             });
386         }
387     });
388     
389     instance.web.ListEditableFormView = instance.web.FormView.extend({
390         init: function() {
391             this._super.apply(this, arguments);
392             this.rendering_engine = new instance.web.ListEditableRenderingEngine(this);
393             this.options.initial_mode = "edit";
394         },
395         renderElement: function() {}
396     });
397     
398     instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
399         init: function(view) {
400             this.view = view;
401         },
402         set_fields_view: function(fields_view) {
403             this.fvg = fields_view;
404         },
405         set_tags_registry: function(tags_registry) {
406             this.tags_registry = tags_registry;
407         },
408         set_fields_registry: function(fields_registry) {
409             this.fields_registry = fields_registry;
410         },
411         render_to: function($element) {
412             var self = this;
413     
414             var xml = instance.web.json_node_to_xml(this.fvg.arch);
415             var $xml = $(xml);
416             
417             if (this.view.editable_list.options.selectable)
418                 $("<td>").appendTo($element);
419                 
420             $xml.children().each(function(i, el) {
421                 var modifiers = JSON.parse($(el).attr("modifiers") || "{}");
422                 var $td = $("<td>");
423                 if (modifiers.tree_invisible === true)
424                     $td.hide();
425                 var tag_name = el.tagName.toLowerCase();
426                 var w;
427                 if (tag_name === "field") {
428                     var name = $(el).attr("name");
429                     var key = $(el).attr('widget') || self.fvg.fields[name].type;
430                     var obj = self.view.fields_registry.get_object(key);
431                     w = new (obj)(self.view, instance.web.xml_to_json(el));
432                     self.view.register_field(w, $(el).attr("name"));
433                 } else {
434                     var obj = self.tags_registry.get_object(tag_name);
435                     w = new (obj)(self.view, instance.web.xml_to_json(el));
436                 }
437                 w.appendTo($td);
438                 $td.appendTo($element);
439             });
440             $(QWeb.render('ListView.row.save')).appendTo($element);
441         },
442     });
443 };