2011-06-03 09:36:06 +00:00
|
|
|
/**
|
2011-09-12 11:34:37 +00:00
|
|
|
* handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
|
|
|
|
* @namespace
|
2011-06-03 09:36:06 +00:00
|
|
|
*/
|
2012-04-17 12:15:59 +00:00
|
|
|
openerp.web.list_editable = function (instance) {
|
2011-06-03 09:36:06 +00:00
|
|
|
// editability status of list rows
|
2012-04-17 12:15:59 +00:00
|
|
|
instance.web.ListView.prototype.defaults.editable = null;
|
2011-06-03 09:36:06 +00:00
|
|
|
|
2011-06-07 10:55:57 +00:00
|
|
|
// TODO: not sure second @lends on existing item is correct, to check
|
2012-04-17 12:15:59 +00:00
|
|
|
instance.web.ListView.include(/** @lends instance.web.ListView# */{
|
2011-06-07 10:55:57 +00:00
|
|
|
init: function () {
|
|
|
|
var self = this;
|
2011-08-08 10:34:24 +00:00
|
|
|
this._super.apply(this, arguments);
|
2012-06-28 14:14:03 +00:00
|
|
|
|
2012-07-24 17:05:50 +00:00
|
|
|
this._force_editability = null;
|
|
|
|
this._context_editable = false;
|
2012-07-17 13:23:08 +00:00
|
|
|
this.editor = this.make_editor();
|
2012-07-10 10:35:57 +00:00
|
|
|
// Stores records of {field, cell}, allows for re-rendering fields
|
|
|
|
// depending on cell state during and after resize events
|
|
|
|
this.fields_for_resize = [];
|
2012-07-17 13:23:08 +00:00
|
|
|
instance.web.bus.on('resize', this, this.resize_fields);
|
2012-07-05 14:37:21 +00:00
|
|
|
|
2011-06-07 10:55:57 +00:00
|
|
|
$(this.groups).bind({
|
|
|
|
'edit': function (e, id, dataset) {
|
|
|
|
self.do_edit(dataset.index, id, dataset);
|
2011-06-08 13:33:01 +00:00
|
|
|
},
|
|
|
|
'saved': function () {
|
|
|
|
if (self.groups.get_selection().length) {
|
|
|
|
return;
|
|
|
|
}
|
2011-10-07 07:20:19 +00:00
|
|
|
self.configure_pager(self.dataset);
|
2011-06-08 13:33:01 +00:00
|
|
|
self.compute_aggregates();
|
2011-06-07 10:55:57 +00:00
|
|
|
}
|
2012-07-10 14:32:28 +00:00
|
|
|
});
|
|
|
|
|
2012-07-18 13:31:15 +00:00
|
|
|
this.records.bind('remove', function () {
|
|
|
|
if (self.editor.is_editing()) {
|
|
|
|
self.cancel_edition();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-07-24 17:05:50 +00:00
|
|
|
this.on('edit:before', this, function (event) {
|
|
|
|
if (!self.editable() || self.editor.is_editing()) {
|
|
|
|
event.cancel = true;
|
|
|
|
}
|
|
|
|
});
|
2012-07-10 14:32:28 +00:00
|
|
|
this.on('edit:after', this, function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
self.$el.add(self.$buttons).addClass('oe_editing');
|
2012-07-10 14:32:28 +00:00
|
|
|
});
|
|
|
|
this.on('save:after cancel:after', this, function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
self.$el.add(self.$buttons).removeClass('oe_editing');
|
2012-07-10 14:32:28 +00:00
|
|
|
});
|
2011-06-07 10:55:57 +00:00
|
|
|
},
|
2012-07-10 10:35:57 +00:00
|
|
|
destroy: function () {
|
2012-07-17 13:23:08 +00:00
|
|
|
instance.web.bus.off('resize', this, this.resize_fields);
|
2012-07-10 10:35:57 +00:00
|
|
|
this._super();
|
|
|
|
},
|
2011-06-07 10:55:57 +00:00
|
|
|
/**
|
|
|
|
* Handles the activation of a record in editable mode (making a record
|
|
|
|
* editable), called *after* the record has become editable.
|
|
|
|
*
|
|
|
|
* The default behavior is to setup the listview's dataset to match
|
|
|
|
* whatever dataset was provided by the editing List
|
|
|
|
*
|
|
|
|
* @param {Number} index index of the record in the dataset
|
|
|
|
* @param {Object} id identifier of the record being edited
|
2012-04-17 12:15:59 +00:00
|
|
|
* @param {instance.web.DataSet} dataset dataset in which the record is available
|
2011-06-07 10:55:57 +00:00
|
|
|
*/
|
|
|
|
do_edit: function (index, id, dataset) {
|
|
|
|
_.extend(this.dataset, dataset);
|
|
|
|
},
|
2012-07-24 17:05:50 +00:00
|
|
|
editable: function () {
|
2012-07-24 17:14:19 +00:00
|
|
|
return this.fields_view.arch.attrs.editable
|
|
|
|
|| this._context_editable
|
|
|
|
|| this.options.editable;
|
2011-06-03 09:36:06 +00:00
|
|
|
},
|
|
|
|
/**
|
2011-09-29 10:32:29 +00:00
|
|
|
* Replace do_search to handle editability process
|
2011-06-03 09:36:06 +00:00
|
|
|
*/
|
2011-09-29 10:40:00 +00:00
|
|
|
do_search: function(domain, context, group_by) {
|
2012-07-24 17:05:50 +00:00
|
|
|
this._context_editable = !!context.set_editable;
|
2011-09-29 10:32:29 +00:00
|
|
|
this._super.apply(this, arguments);
|
2011-06-06 09:59:18 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Replace do_add_record to handle editability (and adding new record
|
|
|
|
* as an editable row at the top or bottom of the list)
|
|
|
|
*/
|
|
|
|
do_add_record: function () {
|
2012-07-24 17:05:50 +00:00
|
|
|
if (this.editable()) {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.find('table:first').show();
|
|
|
|
this.$el.find('.oe_view_nocontent').remove();
|
2012-07-17 13:23:08 +00:00
|
|
|
this.start_edition();
|
2011-06-06 09:59:18 +00:00
|
|
|
} else {
|
2011-08-08 10:34:24 +00:00
|
|
|
this._super();
|
2011-06-06 09:59:18 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
on_loaded: function (data, grouped) {
|
2012-07-02 09:31:13 +00:00
|
|
|
var self = this;
|
2011-06-06 09:59:18 +00:00
|
|
|
// tree/@editable takes priority on everything else if present.
|
2012-06-27 14:32:28 +00:00
|
|
|
var result = this._super(data, grouped);
|
2012-07-24 17:05:50 +00:00
|
|
|
if (this.editable()) {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.addClass('oe_list_editable');
|
2012-07-10 14:32:28 +00:00
|
|
|
// FIXME: any hook available to ensure this is only done once?
|
|
|
|
this.$buttons
|
2012-07-11 14:20:48 +00:00
|
|
|
.off('click', '.oe_list_save')
|
2012-07-17 13:23:08 +00:00
|
|
|
.on('click', '.oe_list_save', this.proxy('save_edition'))
|
2012-07-11 14:20:48 +00:00
|
|
|
.off('click', '.oe_list_discard')
|
|
|
|
.on('click', '.oe_list_discard', function (e) {
|
|
|
|
e.preventDefault();
|
2012-07-17 13:23:08 +00:00
|
|
|
self.cancel_edition();
|
2012-07-11 14:20:48 +00:00
|
|
|
});
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el
|
2012-07-11 12:24:24 +00:00
|
|
|
.off('click', 'tbody td:not(.oe_list_field_cell)')
|
|
|
|
.on('click', 'tbody td:not(.oe_list_field_cell)', function () {
|
2012-07-17 13:23:08 +00:00
|
|
|
if (!self.editor.is_editing()) {
|
|
|
|
self.start_edition();
|
2012-07-11 11:10:27 +00:00
|
|
|
}
|
|
|
|
});
|
2012-07-26 14:33:20 +00:00
|
|
|
this.editor.destroy();
|
2012-07-09 08:06:04 +00:00
|
|
|
// Editor is not restartable due to formview not being
|
|
|
|
// restartable
|
2012-07-17 13:23:08 +00:00
|
|
|
this.editor = this.make_editor();
|
2012-08-24 18:27:07 +00:00
|
|
|
var editor_ready = this.editor.prependTo(this.$el)
|
2012-07-17 13:23:08 +00:00
|
|
|
.then(this.proxy('setup_events'));
|
2012-07-02 09:31:13 +00:00
|
|
|
|
|
|
|
return $.when(result, editor_ready);
|
2012-08-08 10:43:11 +00:00
|
|
|
} else {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.removeClass('oe_list_editable');
|
2012-06-27 14:32:28 +00:00
|
|
|
}
|
|
|
|
|
2012-06-28 14:14:03 +00:00
|
|
|
return result;
|
2011-10-27 08:24:36 +00:00
|
|
|
},
|
2012-07-10 07:39:28 +00:00
|
|
|
/**
|
|
|
|
* Builds a new editor object
|
|
|
|
*
|
|
|
|
* @return {instance.web.list.Editor}
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
make_editor: function () {
|
2012-07-10 07:39:28 +00:00
|
|
|
return new instance.web.list.Editor(this);
|
|
|
|
},
|
2012-08-07 09:23:17 +00:00
|
|
|
do_button_action: function (name, id, callback) {
|
2012-07-10 07:39:28 +00:00
|
|
|
var self = this, args = arguments;
|
2012-08-07 09:23:17 +00:00
|
|
|
this.ensure_saved().then(function (done) {
|
|
|
|
if (!id && done.created) {
|
|
|
|
id = done.record.get('id');
|
|
|
|
}
|
|
|
|
self.handle_button.call(self, name, id, callback);
|
2012-07-03 14:31:26 +00:00
|
|
|
});
|
|
|
|
},
|
2011-10-27 08:24:36 +00:00
|
|
|
/**
|
|
|
|
* Ensures the editable list is saved (saves any pending edition if
|
|
|
|
* needed, or tries to)
|
|
|
|
*
|
|
|
|
* Returns a deferred to the end of the saving.
|
|
|
|
*
|
|
|
|
* @returns {$.Deferred}
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
ensure_saved: function () {
|
|
|
|
if (!this.editor.is_editing()) {
|
2012-06-28 14:14:03 +00:00
|
|
|
return $.when();
|
|
|
|
}
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.save_edition();
|
2012-06-27 14:32:28 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Set up the edition of a record of the list view "inline"
|
|
|
|
*
|
2012-07-02 13:22:17 +00:00
|
|
|
* @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
|
2012-07-19 09:57:42 +00:00
|
|
|
* @param {Object} [options]
|
|
|
|
* @param {String} [options.focus_field] field to focus at start of edition
|
2012-06-28 14:14:03 +00:00
|
|
|
* @return {jQuery.Deferred}
|
2012-06-27 14:32:28 +00:00
|
|
|
*/
|
2012-07-19 09:57:42 +00:00
|
|
|
start_edition: function (record, options) {
|
2012-06-27 14:32:28 +00:00
|
|
|
var self = this;
|
2012-07-16 11:04:30 +00:00
|
|
|
var item = false;
|
|
|
|
if (record) {
|
|
|
|
item = record.attributes;
|
|
|
|
} else {
|
2012-07-09 08:46:52 +00:00
|
|
|
var attrs = {id: false};
|
2012-07-05 14:37:21 +00:00
|
|
|
_(this.columns).chain()
|
|
|
|
.filter(function (x) { return x.tag === 'field'})
|
|
|
|
.pluck('name')
|
|
|
|
.each(function (field) { attrs[field] = false; });
|
|
|
|
record = new instance.web.list.Record(attrs);
|
2012-07-02 13:22:17 +00:00
|
|
|
this.records.add(record, {
|
2012-07-17 13:23:08 +00:00
|
|
|
at: this.prepends_on_create() ? 0 : null});
|
2012-07-02 13:22:17 +00:00
|
|
|
}
|
2012-07-17 13:23:08 +00:00
|
|
|
var $recordRow = this.groups.get_row_for(record);
|
|
|
|
var cells = this.get_cells_for($recordRow);
|
2012-07-05 07:08:54 +00:00
|
|
|
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.ensure_saved().pipe(function () {
|
2012-07-10 10:35:57 +00:00
|
|
|
self.fields_for_resize.splice(0, self.fields_for_resize.length);
|
2012-07-17 13:23:08 +00:00
|
|
|
return self.with_event('edit', {
|
2012-06-28 14:14:03 +00:00
|
|
|
record: record.attributes,
|
|
|
|
cancel: false
|
2012-07-05 07:08:54 +00:00
|
|
|
}, function () {
|
2012-07-16 11:04:30 +00:00
|
|
|
return self.editor.edit(item, function (field_name, field) {
|
2012-07-05 07:08:54 +00:00
|
|
|
var cell = cells[field_name];
|
|
|
|
if (!cell || field.get('effective_readonly')) {
|
2012-07-19 09:57:42 +00:00
|
|
|
// Readonly fields can just remain the list's,
|
|
|
|
// form's usually don't have backgrounds &al
|
2012-07-05 07:08:54 +00:00
|
|
|
field.set({invisible: true});
|
|
|
|
return;
|
|
|
|
}
|
2012-07-03 11:07:00 +00:00
|
|
|
|
2012-07-24 13:56:55 +00:00
|
|
|
// FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
|
2012-08-24 18:27:07 +00:00
|
|
|
field.$el.attr('data-fieldname', field_name);
|
2012-07-10 10:35:57 +00:00
|
|
|
self.fields_for_resize.push({field: field, cell: cell});
|
2012-07-19 09:57:42 +00:00
|
|
|
}, options).pipe(function () {
|
2012-07-05 07:08:54 +00:00
|
|
|
$recordRow.addClass('oe_edition');
|
2012-07-17 13:23:08 +00:00
|
|
|
self.resize_fields();
|
2012-07-05 07:08:54 +00:00
|
|
|
return record.attributes;
|
2012-06-27 14:32:28 +00:00
|
|
|
});
|
2012-07-24 17:05:50 +00:00
|
|
|
}).fail(function () {
|
|
|
|
// if the start_edition event is cancelled and it was a
|
|
|
|
// creation, remove the newly-created empty record
|
|
|
|
if (!record.get('id')) {
|
|
|
|
self.records.remove(record);
|
|
|
|
}
|
2012-07-05 07:08:54 +00:00
|
|
|
});
|
2012-06-28 14:14:03 +00:00
|
|
|
});
|
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
get_cells_for: function ($row) {
|
2012-07-02 13:22:17 +00:00
|
|
|
var cells = {};
|
|
|
|
$row.children('td').each(function (index, el) {
|
|
|
|
cells[el.getAttribute('data-field')] = el
|
|
|
|
});
|
|
|
|
return cells;
|
|
|
|
},
|
2012-07-10 10:35:57 +00:00
|
|
|
/**
|
|
|
|
* If currently editing a row, resizes all registered form fields based
|
|
|
|
* on the corresponding row cell
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
resize_fields: function () {
|
|
|
|
if (!this.editor.is_editing()) { return; }
|
2012-07-10 10:35:57 +00:00
|
|
|
for(var i=0, len=this.fields_for_resize.length; i<len; ++i) {
|
|
|
|
var item = this.fields_for_resize[i];
|
2012-07-17 13:23:08 +00:00
|
|
|
this.resize_field(item.field, item.cell);
|
2012-07-10 10:35:57 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Resizes a field's root element based on the corresponding cell of
|
|
|
|
* a listview row
|
|
|
|
*
|
|
|
|
* @param {instance.web.form.AbstractField} field
|
|
|
|
* @param {jQuery} cell
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
resize_field: function (field, cell) {
|
2012-07-10 10:35:57 +00:00
|
|
|
var $cell = $(cell);
|
|
|
|
var position = $cell.position();
|
|
|
|
|
2012-08-08 19:38:42 +00:00
|
|
|
// jquery does not understand !important
|
2012-08-24 18:27:07 +00:00
|
|
|
field.$el.attr('style', 'width: '+$cell.outerWidth()+'px !important')
|
|
|
|
field.$el.css({
|
2012-07-10 10:35:57 +00:00
|
|
|
top: position.top,
|
|
|
|
left: position.left,
|
|
|
|
minHeight: $cell.outerHeight()
|
|
|
|
});
|
|
|
|
},
|
2012-06-28 14:14:03 +00:00
|
|
|
/**
|
|
|
|
* @return {jQuery.Deferred}
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
save_edition: function () {
|
2012-06-28 14:14:03 +00:00
|
|
|
var self = this;
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.with_event('save', {
|
2012-06-28 14:14:03 +00:00
|
|
|
editor: this.editor,
|
|
|
|
form: this.editor.form,
|
|
|
|
cancel: false
|
2012-07-05 07:08:54 +00:00
|
|
|
}, function () {
|
|
|
|
return this.editor.save().pipe(function (attrs) {
|
|
|
|
var created = false;
|
|
|
|
var record = self.records.get(attrs.id);
|
|
|
|
if (!record) {
|
|
|
|
// new record
|
|
|
|
created = true;
|
|
|
|
record = self.records.find(function (r) {
|
|
|
|
return !r.get('id');
|
|
|
|
}).set('id', attrs.id);
|
|
|
|
}
|
|
|
|
// onwrite callback could be altering & reloading the
|
|
|
|
// record which has *just* been saved, so first perform all
|
|
|
|
// onwrites then do a final reload of the record
|
2012-07-17 13:23:08 +00:00
|
|
|
return self.handle_onwrite(record)
|
2012-07-05 07:08:54 +00:00
|
|
|
.pipe(function () {
|
|
|
|
return self.reload_record(record); })
|
|
|
|
.pipe(function () {
|
|
|
|
return { created: created, record: record }; });
|
2012-07-03 09:10:58 +00:00
|
|
|
});
|
2012-06-28 14:14:03 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
2012-08-06 15:00:02 +00:00
|
|
|
* @param {Boolean} [force=false] discards the data even if the form has been edited
|
2012-06-28 14:14:03 +00:00
|
|
|
* @return {jQuery.Deferred}
|
|
|
|
*/
|
2012-08-06 15:00:02 +00:00
|
|
|
cancel_edition: function (force) {
|
2012-06-28 14:14:03 +00:00
|
|
|
var self = this;
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.with_event('cancel', {
|
2012-06-28 14:14:03 +00:00
|
|
|
editor: this.editor,
|
|
|
|
form: this.editor.form,
|
|
|
|
cancel: false
|
2012-07-05 07:08:54 +00:00
|
|
|
}, function () {
|
2012-08-06 15:00:02 +00:00
|
|
|
return this.editor.cancel(force).pipe(function (attrs) {
|
2012-07-05 07:08:54 +00:00
|
|
|
if (attrs.id) {
|
2012-07-18 13:31:15 +00:00
|
|
|
var record = self.records.get(attrs.id);
|
|
|
|
if (!record) {
|
|
|
|
// Record removed by third party during edition
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return self.reload_record(record);
|
2012-07-05 07:08:54 +00:00
|
|
|
}
|
|
|
|
var to_delete = self.records.find(function (r) {
|
|
|
|
return !r.get('id');
|
|
|
|
});
|
|
|
|
if (to_delete) {
|
|
|
|
self.records.remove(to_delete);
|
|
|
|
}
|
2012-07-03 10:27:31 +00:00
|
|
|
});
|
2012-06-28 14:14:03 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Executes an action on the view's editor bracketed by a cancellable
|
|
|
|
* event of the name provided.
|
|
|
|
*
|
|
|
|
* The event name provided will be post-fixed with ``:before`` and
|
|
|
|
* ``:after``, the ``event`` parameter will be passed alongside the
|
|
|
|
* ``:before`` variant and if the parameter's ``cancel`` key is set to
|
|
|
|
* ``true`` the action *will not be called* and the method will return
|
|
|
|
* a rejection
|
|
|
|
*
|
|
|
|
* @param {String} event_name name of the event
|
|
|
|
* @param {Object} event event object, provided to ``:before`` sub-event
|
|
|
|
* @param {Function} action callable, called with the view's editor as its context
|
|
|
|
* @param {Array} [args] supplementary arguments provided to the action
|
|
|
|
* @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
|
|
|
|
* @return {jQuery.Deferred}
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
with_event: function (event_name, event, action) {
|
2012-06-28 14:14:03 +00:00
|
|
|
var self = this;
|
|
|
|
event = event || {};
|
|
|
|
this.trigger(event_name + ':before', event);
|
|
|
|
if (event.cancel) {
|
2012-07-04 13:28:22 +00:00
|
|
|
return $.Deferred().reject({
|
|
|
|
message: _.str.sprintf("Event %s:before cancelled",
|
|
|
|
event_name)});
|
2012-06-28 14:14:03 +00:00
|
|
|
}
|
2012-07-05 07:08:54 +00:00
|
|
|
return $.when(action.call(this)).then(function () {
|
2012-06-28 14:14:03 +00:00
|
|
|
self.trigger.apply(self, [event_name + ':after']
|
|
|
|
.concat(_.toArray(arguments)));
|
|
|
|
});
|
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
edition_view: function (editor) {
|
2012-06-28 14:14:03 +00:00
|
|
|
var view = $.extend(true, {}, this.fields_view);
|
|
|
|
view.arch.tag = 'form';
|
|
|
|
_.extend(view.arch.attrs, {
|
|
|
|
'class': 'oe_form_container',
|
|
|
|
version: '7.0'
|
|
|
|
});
|
|
|
|
_(view.arch.children).each(function (widget) {
|
|
|
|
var modifiers = JSON.parse(widget.attrs.modifiers || '{}');
|
|
|
|
widget.attrs.nolabel = true;
|
|
|
|
if (modifiers['tree_invisible'] || widget.tag === 'button') {
|
|
|
|
modifiers.invisible = true;
|
|
|
|
}
|
|
|
|
widget.attrs.modifiers = JSON.stringify(modifiers);
|
|
|
|
});
|
|
|
|
return view;
|
2012-07-02 13:58:24 +00:00
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
handle_onwrite: function (source_record) {
|
2012-07-02 13:58:24 +00:00
|
|
|
var self = this;
|
|
|
|
var on_write_callback = self.fields_view.arch.attrs.on_write;
|
|
|
|
if (!on_write_callback) { return $.when(); }
|
|
|
|
return this.dataset.call(on_write_callback, [source_record.get('id')])
|
|
|
|
.pipe(function (ids) {
|
2012-07-03 15:53:05 +00:00
|
|
|
return $.when.apply(
|
|
|
|
null, _(ids).map(
|
2012-07-17 13:23:08 +00:00
|
|
|
_.bind(self.handle_onwrite_record, self, source_record)));
|
2012-07-02 13:58:24 +00:00
|
|
|
});
|
|
|
|
},
|
2012-07-18 10:17:44 +00:00
|
|
|
handle_onwrite_record: function (source_record, id) {
|
2012-07-03 15:53:05 +00:00
|
|
|
var record = this.records.get(id);
|
|
|
|
if (!record) {
|
|
|
|
// insert after the source record
|
|
|
|
var index = this.records.indexOf(source_record) + 1;
|
|
|
|
record = new instance.web.list.Record({id: id});
|
|
|
|
this.records.add(record, {at: index});
|
|
|
|
this.dataset.ids.splice(index, 0, id);
|
|
|
|
}
|
|
|
|
return this.reload_record(record);
|
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
prepends_on_create: function () {
|
2012-07-24 17:05:50 +00:00
|
|
|
return this.editable() === 'top';
|
2012-07-03 15:53:05 +00:00
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
setup_events: function () {
|
2012-07-03 15:53:05 +00:00
|
|
|
var self = this;
|
2012-08-24 18:27:07 +00:00
|
|
|
this.editor.$el.on('keyup keydown', function (e) {
|
2012-07-18 12:43:59 +00:00
|
|
|
if (!self.editor.is_editing()) { return; }
|
2012-07-03 15:53:05 +00:00
|
|
|
var key = _($.ui.keyCode).chain()
|
|
|
|
.map(function (v, k) { return {name: k, code: v}; })
|
|
|
|
.find(function (o) { return o.code === e.which; })
|
|
|
|
.value();
|
|
|
|
if (!key) { return; }
|
2012-07-18 12:43:59 +00:00
|
|
|
var method = e.type + '_' + key.name;
|
2012-07-03 15:53:05 +00:00
|
|
|
if (!(method in self)) { return; }
|
|
|
|
self[method](e);
|
|
|
|
});
|
|
|
|
},
|
2012-07-18 13:07:51 +00:00
|
|
|
/**
|
|
|
|
* Saves the current record, and goes to the next one (creation or
|
|
|
|
* edition)
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
|
2012-07-24 13:56:55 +00:00
|
|
|
* @param {Object} [options]
|
|
|
|
* @param {String} [options.focus_field]
|
2012-07-18 13:07:51 +00:00
|
|
|
* @return {*}
|
|
|
|
*/
|
2012-07-24 13:56:55 +00:00
|
|
|
_next: function (next_record, options) {
|
2012-07-18 13:07:51 +00:00
|
|
|
next_record = next_record || 'succ';
|
2012-07-03 15:53:05 +00:00
|
|
|
var self = this;
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.save_edition().pipe(function (saveInfo) {
|
2012-07-03 15:53:05 +00:00
|
|
|
if (saveInfo.created) {
|
2012-07-17 13:23:08 +00:00
|
|
|
return self.start_edition();
|
2012-07-03 15:53:05 +00:00
|
|
|
}
|
2012-07-24 13:56:55 +00:00
|
|
|
var record = self.records[next_record](
|
|
|
|
saveInfo.record, {wraparound: true});
|
|
|
|
return self.start_edition(record, options);
|
2012-07-03 15:53:05 +00:00
|
|
|
});
|
|
|
|
},
|
2012-07-18 13:07:51 +00:00
|
|
|
keyup_ENTER: function () {
|
|
|
|
return this._next();
|
|
|
|
},
|
2012-07-03 15:53:05 +00:00
|
|
|
keyup_ESCAPE: function () {
|
2012-07-17 13:23:08 +00:00
|
|
|
return this.cancel_edition();
|
2012-07-18 12:43:59 +00:00
|
|
|
},
|
2012-07-26 12:38:19 +00:00
|
|
|
/**
|
|
|
|
* Gets the selection range (start, end) for the provided element,
|
|
|
|
* returns ``null`` if it can't get a range.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
2012-07-18 12:43:59 +00:00
|
|
|
_text_selection_range: function (el) {
|
2012-07-26 12:38:19 +00:00
|
|
|
var selectionStart;
|
|
|
|
try {
|
|
|
|
selectionStart = el.selectionStart;
|
|
|
|
} catch (e) {
|
|
|
|
// radio or checkbox throw on selectionStart access
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (selectionStart !== undefined) {
|
2012-07-18 12:43:59 +00:00
|
|
|
return {
|
2012-07-26 12:38:19 +00:00
|
|
|
start: selectionStart,
|
2012-07-18 12:43:59 +00:00
|
|
|
end: el.selectionEnd
|
|
|
|
};
|
2012-07-26 12:38:19 +00:00
|
|
|
} else if (document.body.createTextRange) {
|
2012-07-18 12:43:59 +00:00
|
|
|
throw new Error("Implement text range handling for MSIE");
|
|
|
|
var sel = document.body.createTextRange();
|
|
|
|
if (sel.parentElement() === el) {
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2012-07-26 12:38:19 +00:00
|
|
|
// Element without selection ranges (select, div/@contenteditable)
|
|
|
|
return null;
|
2012-07-18 12:43:59 +00:00
|
|
|
},
|
|
|
|
_text_cursor: function (el) {
|
|
|
|
var selection = this._text_selection_range(el);
|
2012-07-26 12:38:19 +00:00
|
|
|
if (!selection) {
|
2012-07-18 12:43:59 +00:00
|
|
|
return null;
|
|
|
|
}
|
2012-07-26 12:38:19 +00:00
|
|
|
if (selection.start !== selection.end) {
|
|
|
|
return {position: null, collapsed: false};
|
|
|
|
}
|
|
|
|
return {position: selection.start, collapsed: true};
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Checks if the cursor is at the start of the provided el
|
|
|
|
*
|
|
|
|
* @param {HTMLInputElement | HTMLTextAreaElement}
|
|
|
|
* @returns {Boolean}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_at_start: function (cursor, el) {
|
|
|
|
return cursor.collapsed && (cursor.position === 0);
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Checks if the cursor is at the end of the provided el
|
|
|
|
*
|
|
|
|
* @param {HTMLInputElement | HTMLTextAreaElement}
|
|
|
|
* @returns {Boolean}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_at_end: function (cursor, el) {
|
|
|
|
return cursor.collapsed && (cursor.position === el.value.length);
|
2012-07-18 12:43:59 +00:00
|
|
|
},
|
2012-07-24 13:56:55 +00:00
|
|
|
/**
|
|
|
|
* @param DOMEvent event
|
|
|
|
* @param {String} record_direction direction to move into to get the next record (pred | succ)
|
|
|
|
* @param {Function} is_valid_move whether the edition should be moved to the next record
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_key_move_record: function (event, record_direction, is_valid_move) {
|
2012-07-18 13:07:51 +00:00
|
|
|
if (!this.editor.is_editing('edit')) { return $.when(); }
|
2012-07-26 12:38:19 +00:00
|
|
|
var cursor = this._text_cursor(event.target);
|
|
|
|
// if text-based input (has a cursor)
|
|
|
|
// and selecting (not collapsed) or not at a field boundary
|
|
|
|
// don't move to the next record
|
|
|
|
if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }
|
2012-07-24 13:56:55 +00:00
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
var source_field = $(event.target).closest('[data-fieldname]')
|
|
|
|
.attr('data-fieldname');
|
|
|
|
return this._next(record_direction, {focus_field: source_field});
|
2012-07-18 12:43:59 +00:00
|
|
|
|
2012-07-24 13:56:55 +00:00
|
|
|
},
|
|
|
|
keydown_UP: function (e) {
|
2012-07-26 12:38:19 +00:00
|
|
|
var self = this;
|
|
|
|
return this._key_move_record(e, 'pred', function (el, cursor) {
|
|
|
|
return self._at_start(cursor, el);
|
2012-07-24 13:56:55 +00:00
|
|
|
});
|
2012-07-18 12:43:59 +00:00
|
|
|
},
|
|
|
|
keydown_DOWN: function (e) {
|
2012-07-26 12:38:19 +00:00
|
|
|
var self = this;
|
|
|
|
return this._key_move_record(e, 'succ', function (el, cursor) {
|
|
|
|
return self._at_end(cursor, el);
|
2012-07-24 13:56:55 +00:00
|
|
|
});
|
2012-07-18 13:07:51 +00:00
|
|
|
},
|
2012-07-24 15:01:12 +00:00
|
|
|
|
|
|
|
keydown_LEFT: function (e) {
|
|
|
|
// If the cursor is at the beginning of the field
|
|
|
|
var source_field = $(e.target).closest('[data-fieldname]')
|
|
|
|
.attr('data-fieldname');
|
2012-07-26 12:38:19 +00:00
|
|
|
var cursor = this._text_cursor(e.target);
|
|
|
|
if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }
|
2012-07-24 15:01:12 +00:00
|
|
|
|
|
|
|
var fields_order = this.editor.form.fields_order;
|
|
|
|
var field_index = _(fields_order).indexOf(source_field);
|
|
|
|
|
|
|
|
// Look for the closest visible form field to the left
|
|
|
|
var fields = this.editor.form.fields;
|
|
|
|
var field;
|
|
|
|
do {
|
|
|
|
if (--field_index < 0) { return $.when(); }
|
|
|
|
|
|
|
|
field = fields[fields_order[field_index]];
|
2012-08-24 18:27:07 +00:00
|
|
|
} while (!field.$el.is(':visible'));
|
2012-07-24 15:01:12 +00:00
|
|
|
|
|
|
|
// and focus it
|
|
|
|
field.focus();
|
|
|
|
return $.when();
|
|
|
|
},
|
|
|
|
keydown_RIGHT: function (e) {
|
|
|
|
// same as above, but with cursor at the end of the field and
|
|
|
|
// looking for new fields at the right
|
|
|
|
var source_field = $(e.target).closest('[data-fieldname]')
|
|
|
|
.attr('data-fieldname');
|
2012-07-26 12:38:19 +00:00
|
|
|
var cursor = this._text_cursor(e.target);
|
|
|
|
if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }
|
2012-07-24 15:01:12 +00:00
|
|
|
|
|
|
|
var fields_order = this.editor.form.fields_order;
|
|
|
|
var field_index = _(fields_order).indexOf(source_field);
|
|
|
|
|
|
|
|
var fields = this.editor.form.fields;
|
|
|
|
var field;
|
|
|
|
do {
|
|
|
|
if (++field_index >= fields_order.length) { return $.when(); }
|
|
|
|
|
|
|
|
field = fields[fields_order[field_index]];
|
2012-08-24 18:27:07 +00:00
|
|
|
} while (!field.$el.is(':visible'));
|
2012-07-24 15:01:12 +00:00
|
|
|
|
|
|
|
field.focus();
|
|
|
|
return $.when();
|
|
|
|
},
|
2012-07-18 13:07:51 +00:00
|
|
|
keydown_TAB: function (e) {
|
|
|
|
var form = this.editor.form;
|
|
|
|
var last_field = _(form.fields_order).chain()
|
|
|
|
.map(function (name) { return form.fields[name]; })
|
2012-08-24 18:27:07 +00:00
|
|
|
.filter(function (field) { return field.$el.is(':visible'); })
|
2012-07-18 13:07:51 +00:00
|
|
|
.last()
|
|
|
|
.value();
|
|
|
|
// tabbed from last field in form
|
2012-08-24 18:27:07 +00:00
|
|
|
if (last_field && last_field.$el.has(e.target).length) {
|
2012-07-18 13:07:51 +00:00
|
|
|
e.preventDefault();
|
|
|
|
return this._next();
|
|
|
|
}
|
|
|
|
return $.when();
|
2012-07-03 13:32:10 +00:00
|
|
|
}
|
2012-06-28 14:14:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
instance.web.list.Editor = instance.web.Widget.extend({
|
|
|
|
/**
|
|
|
|
* @constructs instance.web.list.Editor
|
|
|
|
* @extends instance.web.Widget
|
|
|
|
*
|
|
|
|
* Adapter between listview and formview for editable-listview purposes
|
|
|
|
*
|
|
|
|
* @param {instance.web.Widget} parent
|
|
|
|
* @param {Object} options
|
|
|
|
* @param {instance.web.FormView} [options.formView=instance.web.FormView]
|
2012-07-03 13:32:10 +00:00
|
|
|
* @param {Object} [options.delegate]
|
2012-06-28 14:14:03 +00:00
|
|
|
*/
|
|
|
|
init: function (parent, options) {
|
|
|
|
this._super(parent);
|
|
|
|
this.options = options || {};
|
|
|
|
_.defaults(this.options, {
|
2012-07-03 13:32:10 +00:00
|
|
|
formView: instance.web.FormView,
|
|
|
|
delegate: this.getParent()
|
2012-06-28 14:14:03 +00:00
|
|
|
});
|
2012-07-03 13:32:10 +00:00
|
|
|
this.delegate = this.options.delegate;
|
2012-06-28 14:14:03 +00:00
|
|
|
|
|
|
|
this.record = null;
|
|
|
|
|
|
|
|
this.form = new (this.options.formView)(
|
2012-07-03 13:32:10 +00:00
|
|
|
this, this.delegate.dataset, false, {
|
2012-06-28 14:14:03 +00:00
|
|
|
initial_mode: 'edit',
|
2012-08-02 09:39:17 +00:00
|
|
|
disable_autofocus: true,
|
2012-06-28 14:14:03 +00:00
|
|
|
$buttons: $(),
|
|
|
|
$pager: $()
|
|
|
|
});
|
|
|
|
},
|
|
|
|
start: function () {
|
|
|
|
var self = this;
|
|
|
|
var _super = this._super();
|
2012-07-17 13:23:08 +00:00
|
|
|
this.form.embedded_view = this._validate_view(
|
|
|
|
this.delegate.edition_view(this));
|
2012-08-24 18:27:07 +00:00
|
|
|
var form_ready = this.form.appendTo(this.$el).then(
|
2012-07-02 09:31:13 +00:00
|
|
|
self.form.proxy('do_hide'));
|
2012-06-28 14:14:03 +00:00
|
|
|
return $.when(_super, form_ready);
|
|
|
|
},
|
2012-07-17 13:23:08 +00:00
|
|
|
_validate_view: function (edition_view) {
|
2012-07-04 09:56:26 +00:00
|
|
|
if (!edition_view) {
|
2012-07-17 13:23:08 +00:00
|
|
|
throw new Error("editor delegate's #edition_view must return "
|
2012-07-04 09:56:26 +00:00
|
|
|
+ "a view descriptor");
|
|
|
|
}
|
|
|
|
var arch = edition_view.arch;
|
|
|
|
if (!(arch && arch.children instanceof Array)) {
|
2012-07-17 13:23:08 +00:00
|
|
|
throw new Error("Editor delegate's #edition_view must have a" +
|
2012-07-04 09:56:26 +00:00
|
|
|
" non-empty arch")
|
|
|
|
}
|
|
|
|
if (!(arch.tag === "form")) {
|
2012-07-17 13:23:08 +00:00
|
|
|
throw new Error("Editor delegate's #edition_view must have a" +
|
2012-07-04 09:56:26 +00:00
|
|
|
" 'form' root node");
|
|
|
|
}
|
|
|
|
if (!(arch.attrs && arch.attrs.version === "7.0")) {
|
2012-07-17 13:23:08 +00:00
|
|
|
throw new Error("Editor delegate's #edition_view must be a" +
|
2012-07-04 09:56:26 +00:00
|
|
|
" version 7 view");
|
|
|
|
}
|
|
|
|
if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
|
2012-07-17 13:23:08 +00:00
|
|
|
throw new Error("Editor delegate's #edition_view must have the" +
|
2012-07-04 09:56:26 +00:00
|
|
|
" class 'oe_form_container' on its root" +
|
|
|
|
" element");
|
|
|
|
}
|
|
|
|
|
|
|
|
return edition_view;
|
|
|
|
},
|
2012-06-27 14:32:28 +00:00
|
|
|
|
2012-07-18 12:43:59 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {String} [state] either ``new`` or ``edit``
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
|
|
|
is_editing: function (state) {
|
|
|
|
if (!this.record) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
switch(state) {
|
|
|
|
case null: case undefined:
|
|
|
|
return true;
|
|
|
|
case 'new': return !this.record.id;
|
|
|
|
case 'edit': return !!this.record.id;
|
|
|
|
}
|
|
|
|
throw new Error("is_editing's state filter must be either `new` or" +
|
|
|
|
" `edit` if provided");
|
2012-06-28 14:14:03 +00:00
|
|
|
},
|
2012-07-19 09:57:42 +00:00
|
|
|
_focus_setup: function (focus_field) {
|
|
|
|
var form = this.form;
|
|
|
|
|
|
|
|
var field;
|
|
|
|
// If a field to focus was specified
|
|
|
|
if (focus_field
|
|
|
|
// Is actually in the form
|
|
|
|
&& (field = form.fields[focus_field])
|
|
|
|
// And is visible
|
2012-08-24 18:27:07 +00:00
|
|
|
&& field.$el.is(':visible')) {
|
2012-07-19 09:57:42 +00:00
|
|
|
// focus it
|
|
|
|
field.focus();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_(form.fields_order).detect(function (name) {
|
|
|
|
// look for first visible field in fields_order, focus it
|
|
|
|
var field = form.fields[name];
|
2012-08-24 18:27:07 +00:00
|
|
|
if (!field.$el.is(':visible')) {
|
2012-07-19 09:57:42 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Stop as soon as a field got focused
|
2012-08-02 10:00:20 +00:00
|
|
|
return field.focus() !== false;
|
2012-07-19 09:57:42 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
edit: function (record, configureField, options) {
|
2012-07-04 09:56:26 +00:00
|
|
|
// TODO: specify sequence of edit calls
|
2012-06-28 14:14:03 +00:00
|
|
|
var self = this;
|
|
|
|
var form = self.form;
|
2012-07-16 11:04:30 +00:00
|
|
|
var loaded = record
|
|
|
|
? form.on_record_loaded(_.extend({}, record))
|
|
|
|
: form.load_defaults();
|
|
|
|
|
|
|
|
return loaded.pipe(function () {
|
2012-06-28 14:14:03 +00:00
|
|
|
return form.do_show({reload: false});
|
2012-07-03 09:32:00 +00:00
|
|
|
}).pipe(function () {
|
2012-07-16 11:04:30 +00:00
|
|
|
self.record = form.datarecord;
|
2012-06-28 14:14:03 +00:00
|
|
|
_(form.fields).each(function (field, name) {
|
|
|
|
configureField(name, field);
|
|
|
|
});
|
2012-07-19 09:57:42 +00:00
|
|
|
self._focus_setup(options && options.focus_field);
|
2012-06-28 14:14:03 +00:00
|
|
|
return form;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
save: function () {
|
|
|
|
var self = this;
|
|
|
|
return this.form
|
2012-08-29 10:23:00 +00:00
|
|
|
.do_save(this.delegate.prepends_on_create())
|
2012-06-28 14:14:03 +00:00
|
|
|
.pipe(function (result) {
|
|
|
|
var created = result.created && !self.record.id;
|
|
|
|
if (created) {
|
|
|
|
self.record.id = result.result;
|
|
|
|
}
|
|
|
|
return self.cancel();
|
|
|
|
});
|
|
|
|
},
|
2012-08-06 15:00:02 +00:00
|
|
|
cancel: function (force) {
|
|
|
|
if (!(force || this.form.can_be_discarded())) {
|
2012-07-04 13:28:22 +00:00
|
|
|
return $.Deferred().reject({
|
|
|
|
message: "The form's data can not be discarded"}).promise();
|
2012-06-28 14:14:03 +00:00
|
|
|
}
|
2012-08-06 15:00:02 +00:00
|
|
|
var record = this.record;
|
|
|
|
this.record = null;
|
2012-06-28 14:14:03 +00:00
|
|
|
this.form.do_hide();
|
|
|
|
return $.when(record);
|
2011-06-06 09:59:18 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-04-17 12:15:59 +00:00
|
|
|
instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
|
|
|
|
passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved",
|
2012-07-17 13:23:08 +00:00
|
|
|
get_row_for: function (record) {
|
2012-07-02 13:22:17 +00:00
|
|
|
return _(this.children).chain()
|
2012-07-17 13:23:08 +00:00
|
|
|
.invoke('get_row_for', record)
|
2012-07-02 13:22:17 +00:00
|
|
|
.compact()
|
|
|
|
.first()
|
|
|
|
.value();
|
2011-06-03 09:36:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-04-17 12:15:59 +00:00
|
|
|
instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{
|
2011-06-06 11:42:04 +00:00
|
|
|
row_clicked: function (event) {
|
2012-07-24 17:05:50 +00:00
|
|
|
if (!this.view.editable()) {
|
2011-12-14 17:29:52 +00:00
|
|
|
return this._super.apply(this, arguments);
|
2011-06-03 09:36:06 +00:00
|
|
|
}
|
2012-07-10 14:32:28 +00:00
|
|
|
var record_id = $(event.currentTarget).data('id');
|
2012-07-19 09:57:42 +00:00
|
|
|
this.view.start_edition(
|
|
|
|
record_id ? this.records.get(record_id) : null, {
|
|
|
|
focus_field: $(event.target).data('field')
|
|
|
|
});
|
2012-07-02 13:22:17 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* If a row mapping to the record (@data-id matching the record's id or
|
|
|
|
* no @data-id if the record has no id), returns it. Otherwise returns
|
|
|
|
* ``null``.
|
|
|
|
*
|
|
|
|
* @param {Record} record the record to get a row for
|
|
|
|
* @return {jQuery|null}
|
|
|
|
*/
|
2012-07-17 13:23:08 +00:00
|
|
|
get_row_for: function (record) {
|
2012-07-09 08:46:52 +00:00
|
|
|
var id;
|
|
|
|
var $row = this.$current.children('[data-id=' + record.get('id') + ']');
|
2012-07-02 13:22:17 +00:00
|
|
|
if ($row.length) {
|
|
|
|
return $row;
|
|
|
|
}
|
|
|
|
return null;
|
2011-06-03 09:36:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|