[MERGE] forward port of branch 7.0 up to revid 4067 chs@openerp.com-20131114142639...
[odoo/odoo.git] / addons / web_kanban / static / src / js / kanban.js
1 openerp.web_kanban = function (instance) {
2
3 var _t = instance.web._t,
4    _lt = instance.web._lt;
5 var QWeb = instance.web.qweb;
6 instance.web.views.add('kanban', 'instance.web_kanban.KanbanView');
7
8 instance.web_kanban.KanbanView = instance.web.View.extend({
9     template: "KanbanView",
10     display_name: _lt('Kanban'),
11     default_nr_columns: 1,
12     view_type: "kanban",
13     quick_create_class: "instance.web_kanban.QuickCreate",
14     number_of_color_schemes: 10,
15     init: function (parent, dataset, view_id, options) {
16         this._super(parent, dataset, view_id, options);
17         var self = this;
18         _.defaults(this.options, {
19             "quick_creatable": true,
20             "creatable": true,
21             "create_text": undefined,
22             "read_only_mode": false,
23             "confirm_on_delete": true,
24         });
25         this.fields_view = {};
26         this.fields_keys = [];
27         this.group_by = null;
28         this.group_by_field = {};
29         this.grouped_by_m2o = false;
30         this.many2manys = [];
31         this.state = {
32             groups : {},
33             records : {}
34         };
35         this.groups = [];
36         this.aggregates = {};
37         this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
38         this.qweb = new QWeb2.Engine();
39         this.qweb.debug = instance.session.debug;
40         this.qweb.default_dict = _.clone(QWeb.default_dict);
41         this.has_been_loaded = $.Deferred();
42         this.search_domain = this.search_context = this.search_group_by = null;
43         this.currently_dragging = {};
44         this.limit = options.limit || 40;
45         this.add_group_mutex = new $.Mutex();
46     },
47     view_loading: function(r) {
48         return this.load_kanban(r);
49     },
50     start: function() {
51         var self = this;
52         this._super.apply(this, arguments);
53         this.$el.on('click', '.oe_kanban_dummy_cell', function() {
54             if (self.$buttons) {
55                 self.$buttons.find('.oe_kanban_add_column').openerpBounce();
56             }
57         });
58     },
59     destroy: function() {
60         this._super.apply(this, arguments);
61         $('html').off('click.kanban');
62     },
63     load_kanban: function(data) {
64         this.fields_view = data;
65         this.$el.addClass(this.fields_view.arch.attrs['class']);
66         this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
67         if (this.options.$buttons) {
68             this.$buttons.appendTo(this.options.$buttons);
69         } else {
70             this.$el.find('.oe_kanban_buttons').replaceWith(this.$buttons);
71         }
72         this.$buttons
73             .on('click', 'button.oe_kanban_button_new', this.do_add_record)
74             .on('click', '.oe_kanban_add_column', this.do_add_group);
75         this.$groups = this.$el.find('.oe_kanban_groups tr');
76         this.fields_keys = _.keys(this.fields_view.fields);
77         this.add_qweb_template();
78         this.has_been_loaded.resolve();
79         this.trigger('kanban_view_loaded', data);
80         return $.when();
81     },
82     _is_quick_create_enabled: function() {
83         if (!this.options.quick_creatable || !this.is_action_enabled('create'))
84             return false;
85         if (this.fields_view.arch.attrs.quick_create !== undefined)
86             return JSON.parse(this.fields_view.arch.attrs.quick_create);
87         return !! this.group_by;
88     },
89     is_action_enabled: function(action) {
90         if (action === 'create' && !this.options.creatable)
91             return false;
92         return this._super(action);
93     },
94     /*  add_qweb_template
95     *   select the nodes into the xml and send to extract_aggregates the nodes with TagName="field"
96     */
97     add_qweb_template: function() {
98         for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
99             var child = this.fields_view.arch.children[i];
100             if (child.tag === "templates") {
101                 this.transform_qweb_template(child);
102                 this.qweb.add_template(instance.web.json_node_to_xml(child));
103                 break;
104             } else if (child.tag === 'field') {
105                 this.extract_aggregates(child);
106             }
107         }
108     },
109     /*  extract_aggregates
110     *   extract the agggregates from the nodes (TagName="field")
111     */
112     extract_aggregates: function(node) {
113         for (var j = 0, jj = this.group_operators.length; j < jj;  j++) {
114             if (node.attrs[this.group_operators[j]]) {
115                 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
116                 break;
117             }
118         }
119     },
120     transform_qweb_template: function(node) {
121         var qweb_add_if = function(node, condition) {
122             if (node.attrs[QWeb.prefix + '-if']) {
123                 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
124             }
125             node.attrs[QWeb.prefix + '-if'] = condition;
126         };
127         // Process modifiers
128         if (node.tag && node.attrs.modifiers) {
129             var modifiers = JSON.parse(node.attrs.modifiers || '{}');
130             if (modifiers.invisible) {
131                 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
132             }
133         }
134         switch (node.tag) {
135             case 'field':
136                 if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
137                     if (_.indexOf(this.many2manys, node.attrs.name) < 0) {
138                         this.many2manys.push(node.attrs.name);
139                     }
140                     node.tag = 'div';
141                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
142                 } else {
143                     node.tag = QWeb.prefix;
144                     node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
145                 }
146                 break;
147             case 'button':
148             case 'a':
149                 var type = node.attrs.type || '';
150                 if (_.indexOf('action,object,edit,open,delete'.split(','), type) !== -1) {
151                     _.each(node.attrs, function(v, k) {
152                         if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
153                             node.attrs['data-' + k] = v;
154                             delete(node.attrs[k]);
155                         }
156                     });
157                     if (node.attrs['data-string']) {
158                         node.attrs.title = node.attrs['data-string'];
159                     }
160                     if (node.attrs['data-icon']) {
161                         node.children = [{
162                             tag: 'img',
163                             attrs: {
164                                 src: instance.session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
165                                 width: '16',
166                                 height: '16'
167                             }
168                         }];
169                     }
170                     if (node.tag == 'a') {
171                         node.attrs.href = '#';
172                     } else {
173                         node.attrs.type = 'button';
174                     }
175                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
176                 }
177                 break;
178         }
179         if (node.children) {
180             for (var i = 0, ii = node.children.length; i < ii; i++) {
181                 this.transform_qweb_template(node.children[i]);
182             }
183         }
184     },
185     do_add_record: function() {
186         this.dataset.index = null;
187         this.do_switch_view('form');
188     },
189     do_add_group: function() {
190         var self = this;
191         self.do_action({
192             name: _t("Add column"),
193             res_model: self.group_by_field.relation,
194             views: [[false, 'form']],
195             type: 'ir.actions.act_window',
196             target: "new",
197             context: self.dataset.get_context(),
198             flags: {
199                 action_buttons: true,
200             }
201         });
202         var am = instance.webclient.action_manager;
203         var form = am.dialog_widget.views.form.controller;
204         form.on("on_button_cancel", am.dialog, am.dialog.close);
205         form.on('record_created', self, function(r) {
206             (new instance.web.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) {
207                 am.dialog.close();
208                 var domain = self.dataset.domain.slice(0);
209                 domain.push([self.group_by, '=', new_record[0][0]]);
210                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain);
211                 var datagroup = {
212                     get: function(key) {
213                         return this[key];
214                     },
215                     value: new_record[0],
216                     length: 0,
217                     aggregates: {},
218                 };
219                 var new_group = new instance.web_kanban.KanbanGroup(self, [], datagroup, dataset);
220                 self.do_add_groups([new_group]).done(function() {
221                     $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' });
222                 });
223             });
224         });
225     },
226     do_search: function(domain, context, group_by) {
227         var self = this;
228         this.search_domain = domain;
229         this.search_context = context;
230         this.search_group_by = group_by;
231         return $.when(this.has_been_loaded).then(function() {
232             self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
233             self.group_by_field = self.fields_view.fields[self.group_by] || {};
234             self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
235             self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
236             self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
237             var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
238             if (!_.isEmpty(grouping_fields)) {
239                 // ensure group_by fields are read.
240                 self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields));
241             }
242             var grouping = new instance.web.Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields);
243             return self.alive($.when(grouping)).done(function(groups) {
244                 self.remove_no_result();
245                 if (groups) {
246                     self.do_process_groups(groups);
247                 } else {
248                     self.do_process_dataset();
249                 }
250             });
251         });
252     },
253     do_process_groups: function(groups) {
254         var self = this;
255         this.$el.find('table:first').show();
256         this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
257         this.add_group_mutex.exec(function() {
258             self.do_clear_groups();
259             self.dataset.ids = [];
260             if (!groups.length) {
261                 self.no_result();
262                 return false;
263             }
264             self.nb_records = 0;
265             var remaining = groups.length - 1,
266                 groups_array = [];
267             return $.when.apply(null, _.map(groups, function (group, index) {
268                 var def = $.when([]);
269                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
270                     new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
271                 if (group.attributes.length >= 1) {
272                     def = dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit });
273                 }
274                 return def.then(function(records) {
275                         self.nb_records += records.length;
276                         self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
277                         groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
278                         if (!remaining--) {
279                             self.dataset.index = self.dataset.size() ? 0 : null;
280                             return self.do_add_groups(groups_array);
281                         }
282                 });
283             })).then(function () {
284                 if(!self.nb_records) {
285                     self.no_result();
286                 }
287             });
288         });
289     },
290     do_process_dataset: function() {
291         var self = this;
292         this.$el.find('table:first').show();
293         this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
294         this.add_group_mutex.exec(function() {
295             var def = $.Deferred();
296             self.do_clear_groups();
297             self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
298                 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
299                 self.do_add_groups([kgroup]).done(function() {
300                     if (_.isEmpty(records)) {
301                         self.no_result();
302                     }
303                     def.resolve();
304                 });
305             }).done(null, function() {
306                 def.reject();
307             });
308             return def;
309         });
310     },
311     do_reload: function() {
312         this.do_search(this.search_domain, this.search_context, this.search_group_by);
313     },
314     do_clear_groups: function() {
315         var groups = this.groups.slice(0);
316         this.groups = [];
317         _.each(groups, function(group) {
318             group.destroy();
319         });
320     },
321     do_add_groups: function(groups) {
322         var self = this;
323         var $parent = this.$el.parent();
324         this.$el.detach();
325         _.each(groups, function(group) {
326             self.groups[group.undefined_title ? 'unshift' : 'push'](group);
327         });
328         var $last_td = self.$el.find('.oe_kanban_groups_headers td:last');
329         var groups_started = _.map(this.groups, function(group) {
330             if (!group.is_started) {
331                 group.on("add_record", self, function () {
332                     self.remove_no_result();
333                 });
334                 return group.insertBefore($last_td);
335             }
336         });
337         return $.when.apply(null, groups_started).done(function () {
338             self.on_groups_started();
339             self.$el.appendTo($parent);
340             _.each(self.groups, function(group) {
341                 group.compute_cards_auto_height();
342             });
343         });
344     },
345     on_groups_started: function() {
346         var self = this;
347         if (this.group_by) {
348             // Kanban cards drag'n'drop
349             var $columns = this.$el.find('.oe_kanban_column .oe_kanban_column_cards');
350             $columns.sortable({
351                 handle : '.oe_kanban_draghandle',
352                 start: function(event, ui) {
353                     self.currently_dragging.index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
354                     self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
355                     ui.item.find('*').on('click.prevent', function(ev) {
356                         return false;
357                     });
358                     ui.placeholder.height(ui.item.height());
359                 },
360                 revert: 150,
361                 stop: function(event, ui) {
362                     var record = ui.item.data('widget');
363                     var old_index = self.currently_dragging.index;
364                     var new_index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
365                     var old_group = self.currently_dragging.group;
366                     var new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
367                     if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
368                         self.on_record_moved(record, old_group, old_index, new_group, new_index);
369                     }
370                     setTimeout(function() {
371                         // A bit hacky but could not find a better solution for Firefox (problem not present in chrome)
372                         // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop
373                         ui.item.find('*').off('click.prevent');
374                     }, 0);
375                 },
376                 scroll: false
377             });
378             // Keep connectWith out of the sortable initialization for performance sake:
379             // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html
380             $columns.sortable({ connectWith: $columns });
381
382             // Kanban groups drag'n'drop
383             var start_index;
384             if (this.grouped_by_m2o) {
385                 this.$('.oe_kanban_groups_headers').sortable({
386                     items: '.oe_kanban_group_header',
387                     helper: 'clone',
388                     axis: 'x',
389                     opacity: 0.5,
390                     scroll: false,
391                     start: function(event, ui) {
392                         start_index = ui.item.index();
393                         self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'hidden' });
394                     },
395                     stop: function(event, ui) {
396                         var stop_index = ui.item.index();
397                         if (start_index !== stop_index) {
398                             var $start_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(start_index);
399                             var $stop_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index);
400                             var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter';
401                             $start_column[method]($stop_column);
402                             var tmp_group = self.groups.splice(start_index, 1)[0];
403                             self.groups.splice(stop_index, 0, tmp_group);
404                             var new_sequence = _.pluck(self.groups, 'value');
405                             (new instance.web.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) {
406                                 if (r === false) {
407                                     console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation);
408                                 }
409                             });
410                         }
411                         self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'visible' });
412                     }
413                 });
414             }
415         } else {
416             this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
417         }
418         this.postprocess_m2m_tags();
419     },
420     on_record_moved : function(record, old_group, old_index, new_group, new_index) {
421         var self = this;
422         $.fn.tipsy.clear();
423         $(old_group.$el).add(new_group.$el).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
424         if (old_group === new_group) {
425             new_group.records.splice(old_index, 1);
426             new_group.records.splice(new_index, 0, record);
427             new_group.do_save_sequences();
428         } else {
429             old_group.records.splice(old_index, 1);
430             new_group.records.splice(new_index, 0, record);
431             record.group = new_group;
432             var data = {};
433             data[this.group_by] = new_group.value;
434             this.dataset.write(record.id, data, {}).done(function() {
435                 record.do_reload();
436                 new_group.do_save_sequences();
437             }).fail(function(error, evt) {
438                 evt.preventDefault();
439                 alert(_t("An error has occured while moving the record to this group: ") + data.message);
440                 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
441             });
442         }
443     },
444
445     do_show: function() {
446         if (this.$buttons) {
447             this.$buttons.show();
448         }
449         this.do_push_state({});
450         return this._super();
451     },
452     do_hide: function () {
453         if (this.$buttons) {
454             this.$buttons.hide();
455         }
456         return this._super();
457     },
458     open_record: function(id, editable) {
459         if (this.dataset.select_id(id)) {
460             this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
461         } else {
462             this.do_warn("Kanban: could not find id#" + id);
463         }
464     },
465     no_result: function() {
466         var self = this;
467         if (this.groups.group_by
468             || !this.options.action
469             || (!this.options.action.help && !this.options.action.get_empty_list_help)) {
470             return;
471         }
472         this.$el.find('table:first').css("position", "absolute");
473         $(QWeb.render('KanbanView.nocontent', { content : this.options.action.get_empty_list_help || this.options.action.help})).insertAfter(this.$('table:first'));
474         this.$el.find('.oe_view_nocontent').click(function() {
475             self.$buttons.openerpBounce();
476         });
477     },
478     remove_no_result: function() {
479         this.$el.find('table:first').css("position", false);
480         this.$el.find('.oe_view_nocontent').remove();
481     },
482
483     /*
484     *  postprocessing of fields type many2many
485     *  make the rpc request for all ids/model and insert value inside .oe_tags fields
486     */
487     postprocess_m2m_tags: function() {
488         var self = this;
489         if (!this.many2manys.length) {
490             return;
491         }
492         var relations = {};
493         this.groups.forEach(function(group) {
494             group.records.forEach(function(record) {
495                 self.many2manys.forEach(function(name) {
496                     var field = record.record[name];
497                     var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty();
498                     if (!relations[field.relation]) {
499                         relations[field.relation] = { ids: [], elements: {}};
500                     }
501                     var rel = relations[field.relation];
502                     field.raw_value.forEach(function(id) {
503                         rel.ids.push(id);
504                         if (!rel.elements[id]) {
505                             rel.elements[id] = [];
506                         }
507                         rel.elements[id].push($el[0]);
508                     });
509                 });
510             });
511         });
512        _.each(relations, function(rel, rel_name) {
513             var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
514             dataset.name_get(_.uniq(rel.ids)).done(function(result) {
515                 result.forEach(function(nameget) {
516                     $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
517                 });
518             });
519         });
520     }
521 });
522
523
524 function get_class(name) {
525     return new instance.web.Registry({'tmp' : name}).get_object("tmp");
526 }
527
528 instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
529     template: 'KanbanView.group_header',
530     init: function (parent, records, group, dataset) {
531         var self = this;
532         this._super(parent);
533         this.$has_been_started = $.Deferred();
534         this.view = parent;
535         this.group = group;
536         this.dataset = dataset;
537         this.dataset_offset = 0;
538         this.aggregates = {};
539         this.value = this.title = null;
540         if (this.group) {
541             this.value = group.get('value');
542             this.title = group.get('value');
543             if (this.value instanceof Array) {
544                 this.title = this.value[1];
545                 this.value = this.value[0];
546             }
547             var field = this.view.group_by_field;
548             if (!_.isEmpty(field)) {
549                 try {
550                     this.title = instance.web.format_value(group.get('value'), field, false);
551                 } catch(e) {}
552             }
553             _.each(this.view.aggregates, function(value, key) {
554                 self.aggregates[value] = group.get('aggregates')[key];
555             });
556         }
557
558         if (this.title === false) {
559             this.title = _t('Undefined');
560             this.undefined_title = true;
561         }
562         var key = this.view.group_by + '-' + this.value;
563         if (!this.view.state.groups[key]) {
564             this.view.state.groups[key] = {
565                 folded: group ? group.get('folded') : false
566             };
567         }
568         this.state = this.view.state.groups[key];
569         this.$records = null;
570
571         this.records = [];
572         this.$has_been_started.done(function() {
573             self.do_add_records(records);
574         });
575     },
576     start: function() {
577         var self = this,
578             def = this._super();
579         if (! self.view.group_by) {
580             self.$el.addClass("oe_kanban_no_group");
581             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
582                 .on('added', self, self.proxy('quick_created'));
583             self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
584         }
585         this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
586         this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
587
588         this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
589             var fn = 'do_action_' + $(ev.target).data().action;
590             if (typeof(self[fn]) === 'function') {
591                 self[fn]($(ev.target));
592             }
593         });
594
595         this.$el.find('.oe_kanban_add').click(function () {
596             if (self.view.quick) {
597                 self.view.quick.trigger('close');
598             }
599             if (self.quick) {
600                 return false;
601             }
602             self.view.$el.find('.oe_view_nocontent').hide();
603             var ctx = {};
604             ctx['default_' + self.view.group_by] = self.value;
605             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
606                 .on('added', self, self.proxy('quick_created'))
607                 .on('close', self, function() {
608                     self.view.$el.find('.oe_view_nocontent').show();
609                     this.quick.destroy();
610                     delete self.view.quick;
611                     delete this.quick;
612                 });
613             self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
614             self.quick.focus();
615             self.view.quick = self.quick;
616         });
617         // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
618         this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
619         if (this.state.folded) {
620             this.do_toggle_fold();
621         }
622         this.$el.data('widget', this);
623         this.$records.data('widget', this);
624         this.$has_been_started.resolve();
625         var add_btn = this.$el.find('.oe_kanban_add');
626         add_btn.tipsy({delayIn: 500, delayOut: 1000});
627         this.$records.find(".oe_kanban_column_cards").click(function (ev) {
628             if (ev.target == ev.currentTarget) {
629                 if (!self.state.folded) {
630                     add_btn.openerpBounce();
631                 }
632             }
633         });
634         this.is_started = true;
635         return def;
636     },
637     compute_cards_auto_height: function() {
638         // oe_kanban_no_auto_height is an empty class used to disable this feature
639         if (!this.view.group_by) {
640             var min_height = 0;
641             var els = [];
642             _.each(this.records, function(r) {
643                 var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
644                 if ($e.length) {
645                     els.push($e[0]);
646                     min_height = Math.max(min_height, $e.outerHeight());
647                 }
648             });
649             $(els).css('min-height', min_height);
650         }
651     },
652     destroy: function() {
653         this._super();
654         if (this.$records) {
655             this.$records.remove();
656         }
657     },
658     do_show_more: function(evt) {
659         var self = this;
660         var ids = self.view.dataset.ids.splice(0);
661         return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
662             'limit': self.view.limit,
663             'offset': self.dataset_offset += self.view.limit
664         }).then(function(records) {
665             self.view.dataset.ids = ids.concat(self.dataset.ids);
666             self.do_add_records(records);
667             self.compute_cards_auto_height();
668             self.view.postprocess_m2m_tags();
669             return records;
670         });
671     },
672     do_add_records: function(records, prepend) {
673         var self = this;
674         var $list_header = this.$records.find('.oe_kanban_group_list_header');
675         var $show_more = this.$records.find('.oe_kanban_show_more');
676         var $cards = this.$records.find('.oe_kanban_column_cards');
677
678         _.each(records, function(record) {
679             var rec = new instance.web_kanban.KanbanRecord(self, record);
680             if (!prepend) {
681                 rec.appendTo($cards);
682                 self.records.push(rec);
683             } else {
684                 rec.prependTo($cards);
685                 self.records.unshift(rec);
686             }
687         });
688         if ($show_more.length) {
689             var size = this.dataset.size();
690             $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
691         }
692     },
693     remove_record: function(id, remove_from_dataset) {
694         for (var i = 0; i < this.records.length; i++) {
695             if (this.records[i]['id'] === id) {
696                 this.records.splice(i, 1);
697                 i--;
698             }
699         }
700     },
701     do_toggle_fold: function(compute_width) {
702         this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
703         this.state.folded = this.$el.is('.oe_kanban_group_folded');
704         this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
705     },
706     do_action_toggle_fold: function() {
707         this.do_toggle_fold();
708     },
709     do_action_edit: function() {
710         var self = this;
711         self.do_action({
712             res_id: this.value,
713             name: _t("Edit column"),
714             res_model: self.view.group_by_field.relation,
715             views: [[false, 'form']],
716             type: 'ir.actions.act_window',
717             target: "new",
718             flags: {
719                 action_buttons: true,
720             }
721         });
722         var am = instance.webclient.action_manager;
723         var form = am.dialog_widget.views.form.controller;
724         form.on("on_button_cancel", am.dialog, am.dialog.close);
725         form.on('record_saved', self, function() {
726             am.dialog.close();
727             self.view.do_reload();
728         });
729     },
730     do_action_delete: function() {
731         var self = this;
732         if (confirm(_t("Are you sure to remove this column ?"))) {
733             (new instance.web.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
734                 self.view.do_reload();
735             });
736         }
737     },
738     do_save_sequences: function() {
739         var self = this;
740         if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
741             var new_sequence = _.pluck(this.records, 'id');
742             self.view.dataset.resequence(new_sequence);
743         }
744     },
745     /**
746      * Handles a newly created record
747      *
748      * @param {id} id of the newly created record
749      */
750     quick_created: function (record) {
751         var id = record, self = this;
752         self.view.remove_no_result();
753         self.trigger("add_record");
754         this.dataset.read_ids([id], this.view.fields_keys)
755             .done(function (records) {
756                 self.view.dataset.ids.push(id);
757                 self.do_add_records(records, true);
758             });
759     }
760 });
761
762 instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
763     template: 'KanbanView.record',
764     init: function (parent, record) {
765         this._super(parent);
766         this.group = parent;
767         this.view = parent.view;
768         this.id = null;
769         this.set_record(record);
770         if (!this.view.state.records[this.id]) {
771             this.view.state.records[this.id] = {
772                 folded: false
773             };
774         }
775         this.state = this.view.state.records[this.id];
776     },
777     set_record: function(record) {
778         var self = this;
779         this.id = record.id;
780         this.values = {};
781         _.each(record, function(v, k) {
782             self.values[k] = {
783                 value: v
784             };
785         });
786         this.record = this.transform_record(record);
787     },
788     start: function() {
789         this._super();
790         this.$el.data('widget', this);
791         this.bind_events();
792     },
793     transform_record: function(record) {
794         var self = this,
795             new_record = {};
796         _.each(record, function(value, name) {
797             var r = _.clone(self.view.fields_view.fields[name] || {});
798             if ((r.type === 'date' || r.type === 'datetime') && value) {
799                 r.raw_value = instance.web.auto_str_to_date(value);
800             } else {
801                 r.raw_value = value;
802             }
803             r.value = instance.web.format_value(value, r);
804             new_record[name] = r;
805         });
806         return new_record;
807     },
808     renderElement: function() {
809         this.qweb_context = {
810             instance: instance,
811             record: this.record,
812             widget: this,
813             read_only_mode: this.view.options.read_only_mode,
814         };
815         for (var p in this) {
816             if (_.str.startsWith(p, 'kanban_')) {
817                 this.qweb_context[p] = _.bind(this[p], this);
818             }
819         }
820         var $el = instance.web.qweb.render(this.template, {
821             'widget': this,
822             'content': this.view.qweb.render('kanban-box', this.qweb_context)
823         });
824         this.replaceElement($el);
825     },
826     bind_events: function() {
827         var self = this;
828         this.setup_color_picker();
829         this.$el.find('[tooltip]').tipsy({
830             delayIn: 500,
831             delayOut: 0,
832             fade: true,
833             title: function() {
834                 var template = $(this).attr('tooltip');
835                 if (!self.view.qweb.has_template(template)) {
836                     return false;
837                 }
838                 return self.view.qweb.render(template, self.qweb_context);
839             },
840             gravity: 's',
841             html: true,
842             opacity: 0.8,
843             trigger: 'hover'
844         });
845
846         // If no draghandle is found, make the whole card as draghandle (provided one can edit)
847         if (!this.$el.find('.oe_kanban_draghandle').length) {
848             this.$el.children(':first')
849                 .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
850         }
851
852         this.$el.find('.oe_kanban_action').click(function(ev) {
853             ev.preventDefault();
854             var $action = $(this),
855                 type = $action.data('type') || 'button',
856                 method = 'do_action_' + (type === 'action' ? 'object' : type);
857             if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
858                 self.view.open_record(self.id, true);
859             } else if (_.str.startsWith(type, 'switch_')) {
860                 self.view.do_switch_view(type.substr(7));
861             } else if (typeof self[method] === 'function') {
862                 self[method]($action);
863             } else {
864                 self.do_warn("Kanban: no action for type : " + type);
865             }
866         });
867
868         if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
869             this.$el.on('click', function(ev) {
870                 if (!ev.isTrigger && !$._data(ev.target, 'events')) {
871                     var trigger = true;
872                     var elem = ev.target;
873                     var ischild = true;
874                     var children = [];
875                     while (elem) {
876                         var events = $._data(elem, 'events');
877                         if (elem == ev.currentTarget) {
878                             ischild = false;
879                         }
880                         if (ischild) {
881                             children.push(elem);
882                             if (events && events.click) {
883                                 // do not trigger global click if one child has a click event registered
884                                 trigger = false;
885                             }
886                         }
887                         if (trigger && events && events.click) {
888                             _.each(events.click, function(click_event) {
889                                 if (click_event.selector) {
890                                     // For each parent of original target, check if a
891                                     // delegated click is bound to any previously found children
892                                     _.each(children, function(child) {
893                                         if ($(child).is(click_event.selector)) {
894                                             trigger = false;
895                                         }
896                                     });
897                                 }
898                             });
899                         }
900                         elem = elem.parentElement;
901                     }
902                     if (trigger) {
903                         self.on_card_clicked(ev);
904                     }
905                 }
906             });
907         }
908     },
909     /* actions when user click on the block with a specific class
910      *  open on normal view : oe_kanban_global_click
911      *  open on form/edit view : oe_kanban_global_click_edit
912      */
913     on_card_clicked: function(ev) {
914         if(this.$el.find('.oe_kanban_global_click_edit').size()>0)
915             this.do_action_edit();
916         else
917             this.do_action_open();
918     },
919     setup_color_picker: function() {
920         var self = this;
921         var $el = this.$el.find('ul.oe_kanban_colorpicker');
922         if ($el.length) {
923             $el.html(QWeb.render('KanbanColorPicker', {
924                 widget: this
925             }));
926             $el.on('click', 'a', function(ev) {
927                 ev.preventDefault();
928                 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
929                 var data = {};
930                 data[color_field] = $(this).data('color');
931                 self.view.dataset.write(self.id, data, {}).done(function() {
932                     self.record[color_field] = $(this).data('color');
933                     self.do_reload();
934                 });
935             });
936         }
937     },
938     do_action_delete: function($action) {
939         var self = this;
940         function do_it() {
941             return $.when(self.view.dataset.unlink([self.id])).done(function() {
942                 self.group.remove_record(self.id);
943                 self.destroy();
944             });
945         }
946         if (this.view.options.confirm_on_delete) {
947             if (confirm(_t("Are you sure you want to delete this record ?"))) {
948                 return do_it();
949             }
950         } else
951             return do_it();
952     },
953     do_action_edit: function($action) {
954         this.view.open_record(this.id, true);
955     },
956     do_action_open: function($action) {
957         this.view.open_record(this.id);
958     },
959     do_action_object: function ($action) {
960         var button_attrs = $action.data();
961         this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
962     },
963     do_reload: function() {
964         var self = this;
965         this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
966             if (records.length) {
967                 self.set_record(records[0]);
968                 self.renderElement();
969                 self.$el.data('widget', self);
970                 self.bind_events();
971                 self.group.compute_cards_auto_height();
972                 self.view.postprocess_m2m_tags();
973             } else {
974                 self.destroy();
975             }
976         });
977     },
978     kanban_getcolor: function(variable) {
979         var index = 0;
980         switch (typeof(variable)) {
981             case 'string':
982                 for (var i=0, ii=variable.length; i<ii; i++) {
983                     index += variable.charCodeAt(i);
984                 }
985                 break;
986             case 'number':
987                 index = Math.round(variable);
988                 break;
989             default:
990                 return '';
991         }
992         var color = (index % this.view.number_of_color_schemes);
993         return color;
994     },
995     kanban_color: function(variable) {
996         var color = this.kanban_getcolor(variable);
997         return color === '' ? '' : 'oe_kanban_color_' + color;
998     },
999     kanban_gravatar: function(email, size) {
1000         size = size || 22;
1001         email = _.str.trim(email || '').toLowerCase();
1002         var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
1003         var email_md5 = $.md5(email);
1004         return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
1005     },
1006     kanban_image: function(model, field, id, cache, options) {
1007         options = options || {};
1008         var url;
1009         if (this.record[field] && this.record[field].value && !instance.web.form.is_bin_size(this.record[field].value)) {
1010             url = 'data:image/png;base64,' + this.record[field].value;
1011         } else if (this.record[field] && ! this.record[field].value) {
1012             url = "/web/static/src/img/placeholder.png";
1013         } else {
1014             id = JSON.stringify(id);
1015             if (options.preview_image)
1016                 field = options.preview_image;
1017             url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
1018             if (cache !== undefined) {
1019                 // Set the cache duration in seconds.
1020                 url += '&cache=' + parseInt(cache, 10);
1021             }
1022         }
1023         return url;
1024     },
1025     kanban_text_ellipsis: function(s, size) {
1026         size = size || 160;
1027         if (!s) {
1028             return '';
1029         } else if (s.length <= size) {
1030             return s;
1031         } else {
1032             return s.substr(0, size) + '...';
1033         }
1034     },
1035     kanban_compute_domain: function(domain) {
1036         return instance.web.form.compute_domain(domain, this.values);
1037     }
1038 });
1039
1040 /**
1041  * Quick creation view.
1042  *
1043  * Triggers a single event "added" with a single parameter "name", which is the
1044  * name entered by the user
1045  *
1046  * @class
1047  * @type {*}
1048  */
1049 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
1050     template: 'KanbanView.quick_create',
1051     
1052     /**
1053      * close_btn: If true, the widget will display a "Close" button able to trigger
1054      * a "close" event.
1055      */
1056     init: function(parent, dataset, context, buttons) {
1057         this._super(parent);
1058         this._dataset = dataset;
1059         this._buttons = buttons || false;
1060         this._context = context || {};
1061     },
1062     start: function () {
1063         var self = this;
1064         self.$input = this.$el.find('input');
1065         self.$input.keyup(function(event){
1066             if(event.keyCode == 13){
1067                 self.quick_add();
1068             }
1069         });
1070         $(".oe_kanban_quick_create_add", this.$el).click(function () {
1071             self.quick_add();
1072             self.focus();
1073         });
1074         $(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
1075             ev.preventDefault();
1076             self.trigger('close');
1077         });
1078         self.$input.keyup(function(e) {
1079             if (e.keyCode == 27 && self._buttons) {
1080                 self.trigger('close');
1081             }
1082         });
1083     },
1084     focus: function() {
1085         this.$el.find('input').focus();
1086     },
1087     /**
1088      * Handles user event from nested quick creation view
1089      */
1090     quick_add: function () {
1091         var self = this;
1092         var val = this.$input.val();
1093         if (/^\s*$/.test(val)) { return; }
1094         this._dataset.call(
1095             'name_create', [val, new instance.web.CompoundContext(
1096                     this._dataset.get_context(), this._context)])
1097             .then(function(record) {
1098                 self.$input.val("");
1099                 self.trigger('added', record[0]);
1100             }, function(error, event) {
1101                 event.preventDefault();
1102                 return self.slow_create();
1103             });
1104     },
1105     slow_create: function() {
1106         var self = this;
1107         var pop = new instance.web.form.SelectCreatePopup(this);
1108         pop.select_element(
1109             self._dataset.model,
1110             {
1111                 title: _t("Create: ") + (this.string || this.name),
1112                 initial_view: "form",
1113                 disable_multiple_selection: true
1114             },
1115             [],
1116             {"default_name": self.$input.val()}
1117         );
1118         pop.on("elements_selected", self, function(element_ids) {
1119             self.$input.val("");
1120             self.trigger('added', element_ids[0]);
1121         });
1122     }
1123 });
1124 };
1125
1126 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: