/** * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out * @namespace */ openerp.web.list_editable = function (instance) { var _t = instance.web._t; // editability status of list rows instance.web.ListView.prototype.defaults.editable = null; // TODO: not sure second @lends on existing item is correct, to check instance.web.ListView.include(/** @lends instance.web.ListView# */{ init: function () { var self = this; this._super.apply(this, arguments); this.saving_mutex = new $.Mutex(); this._force_editability = null; this._context_editable = false; this.editor = this.make_editor(); // Stores records of {field, cell}, allows for re-rendering fields // depending on cell state during and after resize events this.fields_for_resize = []; instance.web.bus.on('resize', this, this.resize_fields); $(this.groups).bind({ 'edit': function (e, id, dataset) { self.do_edit(dataset.index, id, dataset); }, 'saved': function () { if (self.groups.get_selection().length) { return; } self.configure_pager(self.dataset); self.compute_aggregates(); } }); this.records.bind('remove', function () { if (self.editor.is_editing()) { self.cancel_edition(); } }); this.on('edit:before', this, function (event) { if (!self.editable() || self.editor.is_editing()) { event.cancel = true; } }); this.on('edit:after', this, function () { self.$el.add(self.$buttons).addClass('oe_editing'); }); this.on('save:after cancel:after', this, function () { self.$el.add(self.$buttons).removeClass('oe_editing'); }); }, destroy: function () { instance.web.bus.off('resize', this, this.resize_fields); this._super(); }, do_hide: function () { if (this.editor.is_editing()) { this.cancel_edition(true); } this._super(); }, sort_by_column: function (e) { e.stopPropagation(); if (!this.editor.is_editing()) { this._super.apply(this, arguments); } }, /** * 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 * @param {instance.web.DataSet} dataset dataset in which the record is available */ do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, do_delete: function (ids) { var nonfalse = _.compact(ids); var _super = this._super.bind(this); var next = this.editor.is_editing() ? this.cancel_edition(true) : $.when(); return next.then(function () { return _super(nonfalse); }); }, editable: function () { return !this.grouped && !this.options.disable_editable_mode && (this.fields_view.arch.attrs.editable || this._context_editable || this.options.editable); }, /** * Replace do_search to handle editability process */ do_search: function(domain, context, group_by) { var self=this, _super = self._super, args=arguments; var ready = this.editor.is_editing() ? this.cancel_edition(true) : $.when(); return ready.then(function () { self._context_editable = !!context.set_editable; return _super.apply(self, args); }); }, /** * 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 () { if (this.editable()) { this.$el.find('table:first').show(); this.$el.find('.oe_view_nocontent').remove(); this.start_edition(); } else { this._super(); } }, load_list: function (data, grouped) { var self = this; // tree/@editable takes priority on everything else if present. var result = this._super(data, grouped); // In case current editor was started previously, also has to run // when toggling from editable to non-editable in case form widgets // have setup global behaviors expecting themselves to exist // somehow. this.editor.destroy(); // Editor is not restartable due to formview not being restartable this.editor = this.make_editor(); if (this.editable()) { this.$el.addClass('oe_list_editable'); // FIXME: any hook available to ensure this is only done once? this.$buttons .off('click', '.oe_list_save') .on('click', '.oe_list_save', this.proxy('save_edition')) .off('click', '.oe_list_discard') .on('click', '.oe_list_discard', function (e) { e.preventDefault(); self.cancel_edition(); }); var editor_ready = this.editor.prependTo(this.$el) .done(this.proxy('setup_events')); return $.when(result, editor_ready); } else { this.$el.removeClass('oe_list_editable'); } return result; }, /** * Builds a new editor object * * @return {instance.web.list.Editor} */ make_editor: function () { return new instance.web.list.Editor(this); }, do_button_action: function (name, id, callback) { var self = this, args = arguments; this.ensure_saved().done(function (done) { if (!id && done.created) { id = done.record.get('id'); } self.handle_button(name, id, callback); }); }, /** * 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} */ ensure_saved: function () { return this.save_edition(); }, /** * Builds a record with the provided id (``false`` for a creation), * setting all columns with ``false`` value so code which relies on * having an actual value behaves correctly * * @param {*} id * @return {instance.web.list.Record} */ make_empty_record: function (id) { var attrs = {id: id}; _(this.columns).chain() .filter(function (x) { return x.tag === 'field'}) .pluck('name') .each(function (field) { attrs[field] = false; }); return new instance.web.list.Record(attrs); }, /** * Set up the edition of a record of the list view "inline" * * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record * @param {Object} [options] * @param {String} [options.focus_field] field to focus at start of edition * @return {jQuery.Deferred} */ start_edition: function (record, options) { var self = this; var item = false; if (record) { item = record.attributes; } else { record = this.make_empty_record(false); this.records.add(record, { at: this.prepends_on_create() ? 0 : null}); } return this.ensure_saved().then(function () { var $recordRow = self.groups.get_row_for(record); var cells = self.get_cells_for($recordRow); self.fields_for_resize.splice(0, self.fields_for_resize.length); return self.with_event('edit', { record: record.attributes, cancel: false }, function () { return self.editor.edit(item, function (field_name, field) { var cell = cells[field_name]; if (!cell) { return; } // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow field.$el.attr('data-fieldname', field_name); self.fields_for_resize.push({field: field, cell: cell}); }, options).then(function () { $recordRow.addClass('oe_edition'); self.resize_fields(); return record.attributes; }); }).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); } }); }); }, get_cells_for: function ($row) { var cells = {}; $row.children('td').each(function (index, el) { cells[el.getAttribute('data-field')] = el }); return cells; }, /** * If currently editing a row, resizes all registered form fields based * on the corresponding row cell */ resize_fields: function () { if (!this.editor.is_editing()) { return; } for(var i=0, len=this.fields_for_resize.length; i= fields_order.length) { return $.when(); } field = fields[fields_order[field_index]]; } while (!field.$el.is(':visible')); field.focus(); return $.when(); }, keydown_TAB: function (e) { var form = this.editor.form; var last_field = _(form.fields_order).chain() .map(function (name) { return form.fields[name]; }) .filter(function (field) { return field.$el.is(':visible'); }) .last() .value(); // tabbed from last field in form if (last_field && last_field.$el.has(e.target).length) { e.preventDefault(); return this._next(); } return $.when(); } }); 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] * @param {Object} [options.delegate] */ init: function (parent, options) { this._super(parent); this.options = options || {}; _.defaults(this.options, { formView: instance.web.FormView, delegate: this.getParent() }); this.delegate = this.options.delegate; this.record = null; this.form = new (this.options.formView)( this, this.delegate.dataset, false, { initial_mode: 'edit', disable_autofocus: true, $buttons: $(), $pager: $() }); }, start: function () { var self = this; var _super = this._super(); this.form.embedded_view = this._validate_view( this.delegate.edition_view(this)); var form_ready = this.form.appendTo(this.$el).done( self.form.proxy('do_hide')); return $.when(_super, form_ready); }, _validate_view: function (edition_view) { if (!edition_view) { throw new Error("editor delegate's #edition_view must return " + "a view descriptor"); } var arch = edition_view.arch; if (!(arch && arch.children instanceof Array)) { throw new Error("Editor delegate's #edition_view must have a" + " non-empty arch") } if (arch.tag !== "form") { throw new Error("Editor delegate's #edition_view must have a" + " 'form' root node"); } if (!(arch.attrs && arch.attrs.version === "7.0")) { throw new Error("Editor delegate's #edition_view must be a" + " version 7 view"); } if (!/\boe_form_container\b/.test(arch.attrs['class'])) { throw new Error("Editor delegate's #edition_view must have the" + " class 'oe_form_container' on its root" + " element"); } return edition_view; }, /** * * @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"); }, _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 && field.$el.is(':visible')) { // 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]; if (!field.$el.is(':visible')) { return false; } // Stop as soon as a field got focused return field.focus() !== false; }); }, edit: function (record, configureField, options) { // TODO: specify sequence of edit calls var self = this; var form = self.form; var loaded = record ? form.trigger('load_record', _.extend({}, record)) : form.load_defaults(); return $.when(loaded).then(function () { return form.do_show({reload: false}); }).then(function () { self.record = form.datarecord; _(form.fields).each(function (field, name) { configureField(name, field); }); self._focus_setup(options && options.focus_field); return form; }); }, save: function () { var self = this; return this.form .save(this.delegate.prepends_on_create()) .then(function (result) { var created = result.created && !self.record.id; if (created) { self.record.id = result.result; } return self.cancel(); }); }, cancel: function (force) { if (!(force || this.form.can_be_discarded())) { return $.Deferred().reject({ message: _t("The form's data can not be discarded")}).promise(); } var record = this.record; this.record = null; this.form.do_hide(); return $.when(record); } }); instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved", get_row_for: function (record) { return _(this.children).chain() .invoke('get_row_for', record) .compact() .first() .value(); } }); instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ row_clicked: function (event) { if (!this.view.editable() || ! this.view.is_action_enabled('edit')) { return this._super.apply(this, arguments); } var record_id = $(event.currentTarget).data('id'); return this.view.start_edition( record_id ? this.records.get(record_id) : null, { focus_field: $(event.target).data('field') }); }, /** * 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} */ get_row_for: function (record) { var id; var $row = this.$current.children('[data-id=' + record.get('id') + ']'); if ($row.length) { return $row; } return null; } }); };