diff options
| author | Antony Lesuisse <al@openerp.com> | 2012-07-25 10:26:46 +0200 |
|---|---|---|
| committer | Antony Lesuisse <al@openerp.com> | 2012-07-25 10:26:46 +0200 |
| commit | 3235ed8a1896fbf24e61982e4c06b7d7f0f1815b (patch) | |
| tree | c563cc1ae571e10d95804cb99ff0e7e51a452986 /addons/web/static/src/js | |
| parent | 974d4f4f106762dafb82b6d6f7a7a43fd6cdacdb (diff) | |
| parent | a4f1ce69ab01818fce1a23fc3744865bcc4fd85c (diff) | |
[MERGE] trunk
bzr revid: al@openerp.com-20120725082646-ff6ljt0pquvm0dup
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 35 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 19 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 310 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 158 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 929 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 16 |
8 files changed, 976 insertions, 505 deletions
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 40cff06e66a..b5c69f5c82f 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -135,8 +135,12 @@ instance.web.Dialog = instance.web.Widget.extend({ this.$element.dialog('close'); }, on_close: function() { + if (this.__tmp_dialog_destroying) + return; if (this.dialog_options.destroy_on_close) { + this.__tmp_dialog_closing = true; this.destroy(); + this.__tmp_dialog_closing = undefined; } }, on_resized: function() { @@ -145,6 +149,11 @@ instance.web.Dialog = instance.web.Widget.extend({ _.each(this.getChildren(), function(el) { el.destroy(); }); + if (! this.__tmp_dialog_closing) { + this.__tmp_dialog_destroying = true; + this.close(); + this.__tmp_dialog_destroying = undefined; + } if (! this.isDestroyed()) { this.$element.dialog('destroy'); } @@ -248,13 +257,17 @@ instance.web.Loading = instance.web.Widget.extend({ // Block UI after 3s this.long_running_timer = setTimeout(function () { self.blocked_ui = true; - $.blockUI(); + instance.web.blockUI(); }, 3000); } this.count += increment; if (this.count > 0) { - this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + if (instance.connection.debug) { + this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + } else { + this.$element.text(_t("Loading")); + } this.$element.show(); this.getParent().$element.addClass('oe_wait'); } else { @@ -263,7 +276,7 @@ instance.web.Loading = instance.web.Widget.extend({ // Don't unblock if blocked by somebody else if (self.blocked_ui) { this.blocked_ui = false; - $.unblockUI(); + instance.web.unblockUI(); } this.$element.fadeOut(); this.getParent().$element.removeClass('oe_wait'); @@ -274,7 +287,7 @@ instance.web.Loading = instance.web.Widget.extend({ instance.web.DatabaseManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); - this.unblockUIFunction = $.unblockUI; + this.unblockUIFunction = instance.web.unblockUI; $.validator.addMethod('matches', function (s, _, re) { return new RegExp(re).test(s); }, _t("Invalid database name")); @@ -341,16 +354,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ * from unblocking the UI */ blockUI: function () { - $.blockUI(); - $.unblockUI = function () {}; + instance.web.blockUI(); + instance.web.unblockUI = function () {}; }, /** * Reinstates $.unblockUI so third parties can play with blockUI, and * unblocks the UI */ unblockUI: function () { - $.unblockUI = this.unblockUIFunction; - $.unblockUI(); + instance.web.unblockUI = this.unblockUIFunction; + instance.web.unblockUI(); }, /** * Displays an error dialog resulting from the various RPC communications @@ -851,7 +864,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ window.location.href, 'debug'); }); instance.web.dialog($help, {autoOpen: true, - modal: true, width: 960, title: _t("About")}); + modal: true, width: 580, height: 290, resizable: false, title: _t("About")}); }); }, }); @@ -890,7 +903,7 @@ instance.web.Client = instance.web.Widget.extend({ var doc_width = $(document).width(); var offset = $menu.offset(); var menu_width = $menu.width(); - var x = doc_width - offset.left - menu_width - 15; + var x = doc_width - offset.left - menu_width - 2; if (x < 0) { $menu.offset({ left: offset.left + x }).width(menu_width); } @@ -957,10 +970,12 @@ instance.web.WebClient = instance.web.Client.extend({ }, show_login: function() { var self = this; + self.$('.oe_topbar').hide(); self.login.appendTo(self.$element); }, show_application: function() { var self = this; + self.$('.oe_topbar').show(); self.login.$element.hide(); self.menu = new instance.web.Menu(self); self.menu.replace(this.$element.find('.oe_menu_placeholder')); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 7d101874c07..c9015220ba7 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -550,7 +550,15 @@ $.async_when = function() { /** Setup blockui */ if ($.blockUI) { $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; + $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber.gif">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; +} +instance.web.blockUI = function() { + $.blockUI.apply($, arguments); +} +instance.web.unblockUI = function() { + return $.unblockUI.apply($, arguments); } /** Setup default session */ diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index ebe82ddb4b4..f8e669e1b4a 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -376,7 +376,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ exported_fields.unshift({name: 'id', label: 'External ID'}); var export_format = this.$element.find("#export_format").val(); - $.blockUI(); + instance.web.blockUI(); this.session.get_file({ url: '/web/export/' + export_format, data: {data: JSON.stringify({ @@ -387,7 +387,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ import_compat: Boolean( this.$element.find("#import_compat").val()) })}, - complete: $.unblockUI + complete: instance.web.unblockUI }); }, close: function() { diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 165e47d1f20..7e070e406f8 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -330,6 +330,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea }); } + // Launch a search on clicking the oe_searchview_search button + this.$element.on('click', 'button.oe_searchview_search', function (e) { + e.stopImmediatePropagation(); + self.do_search(); + }); + this.$element.on('keydown', '.oe_searchview_input, .oe_searchview_facet', function (e) { switch(e.which) { @@ -1475,7 +1481,17 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ facet_for: function (value) { var self = this; if (value instanceof Array) { - return $.when(facet_from(this, value)); + if (value.length === 2 && _.isString(value[1])) { + return $.when(facet_from(this, value)); + } + if (value.length > 1) { + // more than one search_default m2o id? Should we OR them? + throw new Error( + _("M2O search fields do not currently handle multiple default values")); + } + // there are many cases of {search_default_$m2ofield: [id]}, need + // to handle this as if it were a single value. + value = value[0]; } return this.model.call('name_get', [value], {}).pipe(function (names) { if (_(names).isEmpty()) { return null; } @@ -1669,6 +1685,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({ return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data")); }, load_data:function(){ + if (!instance.webclient) { return $.Deferred().reject(); } var self = this,dashboard_menu = instance.webclient.menu.data.data.children; var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']); var map_data = function(result){ diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index d6ed85fedad..835527b3135 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -239,6 +239,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } } }, + /** + * + * @param {Object} [options] + * @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode. + * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @return {$.Deferred} + */ do_show: function (options) { var self = this; options = options || {}; @@ -253,23 +260,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } this.$element.show().css('visibility', 'hidden'); this.$element.add(this.$buttons).removeClass('oe_form_dirty'); - return this.has_been_loaded.pipe(function() { - var result; - if (self.dataset.index === null) { - // null index means we should start a new record - result = self.on_button_new(); - } else { - result = self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } - }).pipe(self.on_record_loaded); - } - result.pipe(function() { - if (options.editable) { - self.set({mode: "edit"}); + + var shown = this.has_been_loaded; + if (options.reload !== false) { + shown = shown.pipe(function() { + if (self.dataset.index === null) { + // null index means we should start a new record + return self.on_button_new(); } - self.$element.css('visibility', 'visible'); + return self.dataset.read_index(_.keys(self.fields_view.fields), { + context: { 'bin_size': true } + }).pipe(self.on_record_loaded); }); - return result; + } + return shown.pipe(function() { + if (options.editable) { + self.set({mode: "edit"}); + } + self.$element.css('visibility', 'visible'); }); }, do_hide: function () { @@ -330,6 +338,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.$element.add(self.$buttons).removeClass('oe_form_dirty'); }); }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys) + .pipe(this.on_record_loaded); + } + return this.on_record_loaded({}); + }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, @@ -600,22 +622,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM on_button_new: function() { var self = this; this.set({mode: "edit"}); - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { + return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { - var keys = _.keys(self.fields_view.fields); - if (keys.length) { - self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { - def.resolve(); - }); - } else { - self.on_record_loaded({}).then(function() { - def.resolve(); - }); - } + return self.load_defaults(); } }); - return def.promise(); }, on_button_edit: function() { return this.set({mode: "edit"}); @@ -674,6 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM values = {}, first_invalid_field = null; for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; @@ -689,8 +701,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } if (form_invalid) { self.set({'display_invalid_fields': true}); - for (var f in self.fields) { - self.fields[f]._check_css_flags(); + for (var g in self.fields) { + if (!self.fields.hasOwnProperty(g)) { continue; } + self.fields[g]._check_css_flags(); } first_invalid_field.focus(); self.on_invalid(); @@ -722,14 +735,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM });}); }, on_invalid: function() { - var msg = "<ul>"; - _.each(this.fields, function(f) { - if (!f.is_valid()) { - msg += "<li>" + f.string + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); + var warnings = _(this.fields).chain() + .filter(function (f) { return !f.is_valid(); }) + .map(function (f) { + return _.str.sprintf('<li>%s</li>', + _.escape(f.string)); + }).value(); + warnings.unshift('<ul>'); + warnings.push('</ul>'); + this.do_warn("The following fields are invalid :", warnings.join('')); }, on_saved: function(r, success) { if (!r.result) { @@ -807,10 +821,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { - if (_.include(blacklist, key)) + if (_.include(blacklist, key)) { return; - var val = value_.get_value(); - values[key] = val; + } + values[key] = value_.get_value(); }); return values; }, @@ -945,6 +959,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM is_create_mode: function() { return !this.datarecord.id; }, + open_translate_dialog: function(field) { + return this._super(field); + }, }); /** @@ -988,7 +1005,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } }); } - selector = 'form[version!="7.0"] page,form[version!="7.0"]'; }, render_to: function($target) { var self = this; @@ -1122,7 +1138,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - $label = $('<label/>').attr({ + var $label = $('<label/>').attr({ 'for' : name, "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), "string": $field.attr('string'), @@ -1336,7 +1352,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $new_label; }, handle_common_properties: function($new_element, $node) { - var str_modifiers = $node.attr("modifiers") || "{}" + var str_modifiers = $node.attr("modifiers") || "{}"; var modifiers = JSON.parse(str_modifiers); var ic = null; if (modifiers.invisible !== undefined) @@ -1455,24 +1471,27 @@ instance.web.form.compute_domain = function(expr, fields) { */ instance.web.form.InvisibilityChangerMixin = { init: function(field_manager, invisible_domain) { - this._ic_field_manager = field_manager + var self = this; + this._ic_field_manager = field_manager; this._ic_invisible_modifier = invisible_domain; this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = this._ic_invisible_modifier === undefined ? false : - instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); - this.set({"invisible": result}); + var result = self._ic_invisible_modifier === undefined ? false : + instance.web.form.compute_domain( + self._ic_invisible_modifier, + self._ic_field_manager.fields); + self.set({"invisible": result}); }); this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); var check = function() { - if (this.get("invisible") || this.get('force_invisible')) { - this.set({"effective_invisible": true}); + if (self.get("invisible") || self.get('force_invisible')) { + self.set({"effective_invisible": true}); } else { - this.set({"effective_invisible": false}); + self.set({"effective_invisible": false}); } }; this.on('change:invisible', this, check); this.on('change:force_invisible', this, check); - _.bind(check, this)(); + check.call(this); }, start: function() { this.on("change:effective_invisible", this, this._check_visibility); @@ -1536,6 +1555,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi var compute_domain = instance.web.form.compute_domain; var to_set = {}; for (var a in this.modifiers) { + if (!this.modifiers.hasOwnProperty(a)) { continue; } if (!_.include(["invisible"], a)) { var val = compute_domain(this.modifiers[a], this.view.fields); to_set[a] = val; @@ -1682,11 +1702,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ on_confirmed: function() { var self = this; - var context = this.node.attrs.context; - if (context && context.__ref) { - context = new instance.web.CompoundContext(context); - context.set_eval_context(this._build_eval_context()); - } + var context = this.build_context(); return this.view.do_execute_action( _.extend({}, this.node.attrs, {context: context}), @@ -1739,18 +1755,18 @@ instance.web.form.FieldInterface = { /** * Get the current value of the widget. * - * Must always return a syntaxically correct value to be passed to the "write" method of the osv class in + * Must always return a syntactically correct value to be passed to the "write" method of the osv class in * the OpenERP server, although it is not assumed to respect the constraints applied to the field. - * For example if the field is marqued as "required", a call to get_value() can return false. + * For example if the field is marked as "required", a call to get_value() can return false. * * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to - * return a defaut value according to the type of field. + * return a default value according to the type of field. * * This method is always assumed to perform synchronously, it can not return a promise. * * If there was no user interaction to modify the value of the field, it is always assumed that * get_value() return the same semantic value than the one passed in the last call to set_value(), - * altough the syntax can be different. This can be the case for type of fields that have a different + * although the syntax can be different. This can be the case for type of fields that have a different * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). */ get_value: function() {}, @@ -1765,7 +1781,7 @@ instance.web.form.FieldInterface = { */ is_valid: function() {}, /** - * Returns true if the field holds a value which is syntaxically correct, ignoring + * Returns true if the field holds a value which is syntactically correct, ignoring * the potential semantic restrictions applied to the field. */ is_syntax_valid: function() {}, @@ -1795,6 +1811,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w * @param node */ init: function(field_manager, node) { + var self = this this._super(field_manager, node); this.field_manager = field_manager; this.name = this.node.attrs.name; @@ -1809,12 +1826,11 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w this.set({"readonly": this.modifiers['readonly'] === true}); this.set({"force_readonly": false}); var test_effective_readonly = function() { - this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); + self.set({"effective_readonly": self.get("readonly") || !!self.get("force_readonly")}); }; this.on("change:readonly", this, test_effective_readonly); this.on("change:force_readonly", this, test_effective_readonly); - _.bind(test_effective_readonly, this)(); - + test_effective_readonly.call(this); this.on("change:value", this, function() { if (! this._inhibit_on_change) this.trigger('changed_value'); @@ -1891,7 +1907,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w */ delay_focus: function($elem) { setTimeout(function() { - $elem.focus(); + $elem[0].focus(); }, 50); }, /** @@ -2234,6 +2250,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we } else { this.$textarea.attr('disabled', 'disabled'); } + this.$element.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); this.setupFocus(this.$textarea); }, set_value: function(value_) { @@ -2337,6 +2358,7 @@ instance.web.form.FieldTextHtml = instance.web.form.FieldText.extend({ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { + var self = this; this._super.apply(this, arguments); this.$checkbox = $("input", this.$element); this.setupFocus(this.$checkbox); @@ -2344,10 +2366,10 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ this.set({'value': this.$checkbox.is(':checked')}); }, this)); var check_readonly = function() { - this.$checkbox.prop('disabled', this.get("effective_readonly")); + self.$checkbox.prop('disabled', self.get("effective_readonly")); }; this.on("change:effective_readonly", this, check_readonly); - _.bind(check_readonly, this)(); + check_readonly.call(this); }, set_value: function(value_) { this._super.apply(this, arguments); @@ -2960,6 +2982,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ selectable: self.multi_selection, sortable: false, import_enabled: false, + deletable: true }); if (self.get("effective_readonly")) { _.extend(view.options, { @@ -3004,8 +3027,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - if (self.get("effective_readonly")) - controller.set_editable(false); + if (self.get("effective_readonly")) { + controller.on('edit:before', self, function (e) { + e.cancel = true; + }); + } } else if (view_type === "form") { if (self.get("effective_readonly")) { $(".oe_form_buttons", controller.$element).children().remove(); @@ -3189,6 +3215,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form: 'instance.web.form.One2ManyFormView', kanban: 'instance.web.form.One2ManyKanbanView', }); + this.__ignore_blur = false; }, switch_view: function(mode, unused) { if (mode !== 'form') { @@ -3238,19 +3265,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super(parent, dataset, view_id, _.extend(options || {}, { ListType: instance.web.form.One2ManyList })); + this.on('edit:before', this, this.proxy('_before_edit')); + this.on('save:before cancel:before', this, this.proxy('_before_unedit')); + + this.records + .bind('add', this.proxy("changed_records")) + .bind('edit', this.proxy("changed_records")) + .bind('remove', this.proxy("changed_records")); + }, + start: function () { + var ret = this._super(); + this.$element + .off('mousedown.handleButtons') + .on('mousedown.handleButtons', 'table button', this.proxy('_button_down')); + return ret; + }, + changed_records: function () { + this.o2m.trigger_on_change(); }, is_valid: function () { - var form; - // A list not being edited is always valid - if (!(form = this.first_edition_form())) { - return true; - } + var form = this.editor.form; + // If the form has not been modified, the view can only be valid // NB: is_dirty will also be set on defaults/onchanges/whatever? // oe_form_dirty seems to only be set on actual user actions if (!form.$element.is('.oe_form_dirty')) { return true; } + this.o2m._dirty_flag = true; // Otherwise validate internal form return _(form.fields).chain() @@ -3261,21 +3303,8 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ .all(_.identity) .value(); }, - first_edition_form: function () { - var get_form = function (group_or_list) { - if (group_or_list.edition) { - return group_or_list.edition_form; - } - return _(group_or_list.children).chain() - .map(get_form) - .compact() - .first() - .value(); - }; - return get_form(this.groups); - }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -3328,55 +3357,58 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ }); }, do_button_action: function (name, id, callback) { - var _super = _.bind(this._super, this); - - this.o2m.view.do_save().then(function () { - _super(name, id, callback); - }); - } -}); -instance.web.form.One2ManyList = instance.web.ListView.List.extend({ - KEY_RETURN: 13, - // blurring caused by hitting the [Return] key, should skip the - // autosave-on-blur and let the handler for [Return] do its thing - __return_blur: false, - render_row_as_form: function () { + if (!_.isNumber(id)) { + instance.webclient.notification.warn( + _t("Action Button"), + _t("The o2m record must be saved before an action can be used")); + return; + } + var parent_form = this.o2m.view; var self = this; - return this._super.apply(this, arguments).then(function () { - // Replace the "Save Row" button with "Cancel Edition" - self.edition_form.$element - .undelegate('button.oe_list_edit_row_save', 'click') - .delegate('button.oe_list_edit_row_save', 'click', function () { - self.cancel_pending_edition(); - }); - - // Overload execute_action on the edition form to perform a simple - // reload_record after the action is done, rather than fully - // reload the parent view (or something) - var _execute_action = self.edition_form.do_execute_action; - self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) { - return _execute_action.call(this, action, dataset, record_id, function () { - self.view.reload_record( - self.view.records.get(record_id)); - }); - }; - - self.edition_form.on('blurred', null, function () { - if (self.__return_blur) { - delete self.__return_blur; - return; - } - if (!self.edition_form.widget_is_stopped) { - self.view.ensure_saved(); - } - }); + this.ensure_saved().pipe(function () { + return parent_form.do_save(); + }).then(function () { + self.handle_button(name, id, callback); }); }, - on_row_keyup: function (e) { - if (e.which === this.KEY_RETURN) { - this.__return_blur = true; + + _before_edit: function () { + this.__ignore_blur = false; + this.editor.form.on('blurred', this, this._on_form_blur); + }, + _before_unedit: function () { + this.editor.form.off('blurred', this, this._on_form_blur); + }, + _button_down: function () { + // If a button is clicked (usually some sort of action button), it's + // the button's responsibility to ensure the editable list is in the + // correct state -> ignore form blurring + this.__ignore_blur = true; + }, + /** + * Handles blurring of the nested form (saves the currently edited row), + * unless the flag to ignore the event is set to ``true`` + * + * Makes the internal form go away + */ + _on_form_blur: function () { + if (this.__ignore_blur) { + this.__ignore_blur = false; + return; } - this._super(e); + // FIXME: why isn't there an API for this? + if (this.editor.form.$element.hasClass('oe_form_dirty')) { + this.save_edition(); + return; + } + this.cancel_edition(); + }, + keyup_ENTER: function () { + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing (save + // the current row *anyway*, then create a new one/edit the next one) + this.__ignore_blur = true; + this._super.apply(this, arguments); } }); @@ -3545,7 +3577,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.addClass('oe_form_field_many2many'); + this.$element.addClass('oe_form_field oe_form_field_many2many'); var self = this; @@ -4079,10 +4111,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.dataset, false, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, - 'read_only': true, 'import_enabled': false, '$buttons': self.$buttonpane, }, self.options.list_view_options || {})); + self.view_list.on('edit:before', self, function (e) { + e.cancel = true; + }); self.view_list.popup = self; self.view_list.appendTo($(".oe_popup_list", self.$element)).pipe(function() { self.view_list.do_show(); @@ -4314,7 +4348,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. //link.target = '_blank'; link.href = "data:application/octet-stream;base64," + value; } else { - $.blockUI(); + instance.web.blockUI(); this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ @@ -4324,7 +4358,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. filename_field: (this.node.attrs.filename || ''), context: this.view.dataset.get_context() })}, - complete: $.unblockUI, + complete: instance.web.unblockUI, error: instance.webclient.crashmanager.on_rpc_error }); ev.stopPropagation(); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index edc53f82042..53a8c0126b2 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi // whether the view rows can be reordered (via vertical drag & drop) 'reorderable': true, 'action_buttons': true, - // if true, the view can't be editable, ignoring the view's and the context's - // instructions - 'read_only': false, // if true, the 'Import', 'Export', etc... buttons will be shown 'import_enabled': true, }, @@ -142,7 +139,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); }, /** - * View startup method, the default behavior is to set the ``oe_listw`` + * View startup method, the default behavior is to set the ``oe_list`` * class on its root element and to perform an RPC load call. * * @returns {$.Deferred} loading promise @@ -288,7 +285,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } this.$buttons.find('.oe_list_add') .click(this.proxy('do_add_record')) - .prop('disabled', grouped && this.options.editable); + .prop('disabled', grouped); this.$buttons.on('click', '.oe_list_button_import', function() { self.on_sidebar_import(); return false; @@ -411,20 +408,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (column.modifiers) { var modifiers = JSON.parse(column.modifiers); column.modifiers_for = function (fields) { - if (!modifiers.invisible) { - return {}; + var out = {}; + + for (var attr in modifiers) { + if (!modifiers.hasOwnProperty(attr)) { continue; } + var modifier = modifiers[attr]; + out[attr] = _.isBoolean(modifier) + ? modifier + : domain_computer(modifier, fields); } - return { - 'invisible': domain_computer(modifiers.invisible, fields) - }; + + return out; }; if (modifiers['tree_invisible']) { column.invisible = '1'; } else { delete column.invisible; } + column.modifiers = modifiers; } else { column.modifiers_for = noop; + column.modifiers = {}; } return column; }; @@ -436,10 +440,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (grouped) { this.columns.unshift({ id: '_group', tag: '', string: _t("Group"), meta: true, - modifiers_for: function () { return {}; } + modifiers_for: function () { return {}; }, + modifiers: {} }, { id: '_count', tag: '', string: '#', meta: true, - modifiers_for: function () { return {}; } + modifiers_for: function () { return {}; }, + modifiers: {} }); } @@ -667,6 +673,19 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + this.handle_button(name, id, callback); + }, + /** + * Base handling of buttons, can be called when overriding do_button_action + * in order to bypass parent overrides. + * + * This method should not be overridden. + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + handle_button: function (name, id, callback) { var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -911,27 +930,42 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.record_callbacks = { 'remove': function (event, record) { - var $row = self.$current.find( - '[data-id=' + record.get('id') + ']'); + var $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); var index = $row.data('index'); $row.remove(); self.refresh_zebra(index); }, 'reset': function () { return self.on_records_reset(); }, - 'change': function (event, record) { - var $row = self.$current.find('[data-id=' + record.get('id') + ']'); + 'change': function (event, record, attribute, value, old_value) { + var $row; + if (attribute === 'id') { + if (old_value) { + throw new Error("Setting 'id' attribute on existing record " + + JSON.stringify(record.attributes)); + } + if (!_.contains(self.dataset.ids, value)) { + // add record to dataset if not already in (added by + // the form view?) + self.dataset.ids.splice( + self.records.indexOf(record), 0, value); + } + // Set id on new record + $row = self.$current.children('[data-id=false]'); + } else { + $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); + } $row.replaceWith(self.render_record(record)); }, 'add': function (ev, records, record, index) { - var $new_row = $('<tr>').attr({ - 'data-id': record.get('id') - }); + var $new_row = $(self.render_record(record)); if (index === 0) { $new_row.prependTo(self.$current); } else { var previous_record = records.at(index-1), - $previous_sibling = self.$current.find( + $previous_sibling = self.$current.children( '[data-id=' + previous_record.get('id') + ']'); $new_row.insertAfter($previous_sibling); } @@ -975,11 +1009,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. e.stopPropagation(); }) .delegate('tr', 'click', function (e) { - e.stopPropagation(); var row_id = self.row_id(e.currentTarget); - if (row_id !== undefined) { + if (row_id) { + e.stopPropagation(); if (!self.dataset.select_id(row_id)) { - throw "Could not find id in dataset" + throw new Error("Could not find id in dataset"); } self.row_clicked(e); } @@ -1139,6 +1173,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {String} QWeb rendering of the selected record */ render_record: function (record) { + var self = this; var index = this.records.indexOf(record); return QWeb.render('ListView.row', { columns: this.columns, @@ -1147,7 +1182,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. row_parity: (index % 2 === 0) ? 'even' : 'odd', view: this.view, render_cell: function () { - return this.render_cell.apply(this, arguments); } + return self.render_cell.apply(self, arguments); } }); }, /** @@ -1831,7 +1866,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ * @returns this */ remove: function (record) { - var index = _(this.records).indexOf(record); + var index = this.indexOf(record); if (index === -1) { _(this._proxies).each(function (proxy) { proxy.remove(record); @@ -1847,13 +1882,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ return this; }, - _onRecordEvent: function (event, record, options) { + _onRecordEvent: function (event) { + switch(event) { // don't propagate reset events - if (event === 'reset') { return; } + case 'reset': return; + case 'change:id': + var record = arguments[1]; + var new_value = arguments[2]; + var old_value = arguments[3]; + // [change:id, record, new_value, old_value] + if (this._byId[old_value] === record) { + delete this._byId[old_value]; + this._byId[new_value] = record; + } + break; + } this.trigger.apply(this, arguments); }, // underscore-type methods + find: function (callback) { + var record; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue + } + if ((record = this._proxies[section].find(callback))) { + return record; + } + } + for(var i=0; i<this.length; ++i) { + record = this.records[i]; + if (callback(record)) { + return record; + } + } + }, each: function (callback) { for(var section in this._proxies) { if (this._proxies.hasOwnProperty(section)) { @@ -1878,6 +1942,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ }, indexOf: function (record) { return _(this.records).indexOf(record); + }, + succ: function (record, options) { + options = options || {wraparound: false}; + var result; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].succ(record, options))) { + return result; + } + } + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index + 1; + if (options.wraparound && (next_index === this.length)) { + return this.at(0); + } + return this.at(next_index); + }, + pred: function (record, options) { + options = options || {wraparound: false}; + + var result; + for (var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].pred(record, options))) { + return result; + } + } + + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index - 1; + if (options.wraparound && (next_index === -1)) { + return this.at(this.length - 1); + } + return this.at(next_index); } }); Collection.include(Events); diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index c370aeaf7fd..c7654479864 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -3,10 +3,6 @@ * @namespace */ openerp.web.list_editable = function (instance) { - var KEY_RETURN = 13, - KEY_ESCAPE = 27; - var QWeb = instance.web.qweb; - // editability status of list rows instance.web.ListView.prototype.defaults.editable = null; @@ -15,6 +11,15 @@ openerp.web.list_editable = function (instance) { init: function () { var self = this; this._super.apply(this, arguments); + + 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); @@ -26,7 +31,29 @@ openerp.web.list_editable = function (instance) { 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.$element.add(self.$buttons).addClass('oe_editing'); + }); + this.on('save:after cancel:after', this, function () { + self.$element.add(self.$buttons).removeClass('oe_editing'); + }); + }, + destroy: function () { + instance.web.bus.off('resize', this, this.resize_fields); + this._super(); }, /** * Handles the activation of a record in editable mode (making a record @@ -42,25 +69,16 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, - /** - * Sets editability status for the list, based on defaults, view - * architecture and the provided flag, if any. - * - * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". - */ - set_editable: function (force) { - // If ``force``, set editability to bottom - // otherwise rely on view default - // view' @editable is handled separately as we have not yet - // fetched and processed the view at this point. - this.options.editable = ( - ! this.options.read_only && ((force && "bottom") || this.defaults.editable)); + editable: function () { + return 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) { - this.set_editable(context['set_editable']); + this._context_editable = !!context.set_editable; this._super.apply(this, arguments); }, /** @@ -68,18 +86,62 @@ openerp.web.list_editable = function (instance) { * as an editable row at the top or bottom of the list) */ do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this.$element.find('table:first').show(); this.$element.find('.oe_view_nocontent').remove(); - this.groups.new_record(); + this.start_edition(); } else { this._super(); } }, on_loaded: function (data, grouped) { + var self = this; + if (this.editor) { + this.editor.destroy(); + } // tree/@editable takes priority on everything else if present. - this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable); - return this._super(data, grouped); + var result = this._super(data, grouped); + if (this.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(); + }); + this.$element + .off('click', 'tbody td:not(.oe_list_field_cell)') + .on('click', 'tbody td:not(.oe_list_field_cell)', function () { + if (!self.editor.is_editing()) { + self.start_edition(); + } + }); + // Editor is not restartable due to formview not being + // restartable + this.editor = this.make_editor(); + var editor_ready = this.editor.prependTo(this.$element) + .then(this.proxy('setup_events')); + + return $.when(result, editor_ready); + } + + 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 () { + var self = this, args = arguments; + this.ensure_saved().then(function () { + self.handle_button.apply(self, args); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -90,354 +152,581 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - return this.groups.ensure_saved(); - } - }); - - instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", - new_record: function () { - // TODO: handle multiple children - this.children[null].new_record(); + if (!this.editor.is_editing()) { + return $.when(); + } + return this.save_edition(); }, /** - * Ensures descendant editable List instances are all saved if they have - * pending editions. + * Set up the edition of a record of the list view "inline" * - * @returns {$.Deferred} + * @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} */ - ensure_saved: function () { - return $.when.apply(null, - _.invoke( - _.values(this.children), - 'ensure_saved')); - } - }); - - instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ - row_clicked: function (event) { - if (!this.options.editable) { - return this._super.apply(this, arguments); + start_edition: function (record, options) { + var self = this; + var item = false; + if (record) { + item = record.attributes; + } else { + var attrs = {id: false}; + _(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); + this.records.add(record, { + at: this.prepends_on_create() ? 0 : null}); } - this.edit_record($(event.currentTarget).data('id')); + var $recordRow = this.groups.get_row_for(record); + var cells = this.get_cells_for($recordRow); + + return this.ensure_saved().pipe(function () { + 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 || field.get('effective_readonly')) { + // Readonly fields can just remain the list's, + // form's usually don't have backgrounds &al + field.set({invisible: true}); + return; + } + + // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow + field.$element.attr('data-fieldname', field_name); + self.fields_for_resize.push({field: field, cell: cell}); + }, options).pipe(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; }, /** - * Checks if a record is being edited, and if so cancels it + * If currently editing a row, resizes all registered form fields based + * on the corresponding row cell */ - cancel_pending_edition: function () { - var self = this, cancelled; - if (!this.edition) { - return $.when(); + resize_fields: function () { + if (!this.editor.is_editing()) { return; } + for(var i=0, len=this.fields_for_resize.length; i<len; ++i) { + var item = this.fields_for_resize[i]; + this.resize_field(item.field, item.cell); } - - if (this.edition_id) { - cancelled = this.reload_record(this.records.get(this.edition_id)); - } else { - cancelled = $.when(); - } - cancelled.then(function () { - self.view.unpad_columns(); - self.edition_form.destroy(); - self.edition_form.$element.remove(); - delete self.edition_form; - self.dataset.index = null; - delete self.edition_id; - delete self.edition; - }); - this.pad_table_to(5); - return cancelled; }, /** - * Adapts this list's view description to be suitable to the inner form - * view of a row being edited. + * Resizes a field's root element based on the corresponding cell of + * a listview row * - * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + * @param {instance.web.form.AbstractField} field + * @param {jQuery} cell */ - get_form_fields_view: function () { - // deep copy of view - var view = $.extend(true, {}, this.group.view.fields_view); - _(view.arch.children).each(function (widget) { - widget.attrs.nolabel = true; - if (widget.tag === 'button') { - delete widget.attrs.string; - } + resize_field: function (field, cell) { + var $cell = $(cell); + var position = $cell.position(); + + field.$element.css({ + top: position.top, + left: position.left, + width: $cell.outerWidth(), + minHeight: $cell.outerHeight() }); - view.arch.attrs.col = 2 * view.arch.children.length; - return view; }, - on_row_keyup: function (e) { + /** + * @return {jQuery.Deferred} + */ + save_edition: function () { var self = this; - switch (e.which) { - case KEY_RETURN: - $(e.target).blur(); - e.preventDefault(); - //e.stopImmediatePropagation(); - setTimeout(function () { - self.save_row().then(function (result) { - if (result.created) { - self.new_record(); - return; - } - - var next_record_id, - next_record = self.records.at( - self.records.indexOf(result.edited_record) + 1); - if (next_record) { - next_record_id = next_record.get('id'); - self.dataset.index = _(self.dataset.ids) - .indexOf(next_record_id); - } else { - self.dataset.index = 0; - next_record_id = self.records.at(0).get('id'); - } - self.edit_record(next_record_id); - }, 0); + return this.with_event('save', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, 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 + return self.handle_onwrite(record) + .pipe(function () { + return self.reload_record(record); }) + .pipe(function () { + return { created: created, record: record }; }); }); - break; - case KEY_ESCAPE: - this.cancel_edition(); - break; - } + }); }, - render_row_as_form: function (row) { + /** + * @return {jQuery.Deferred} + */ + cancel_edition: function () { var self = this; - return this.ensure_saved().pipe(function () { - var record_id = $(row).data('id'); - var $new_row = $('<tr>', { - id: _.uniqueId('oe-editable-row-'), - 'data-id': record_id, - 'class': (row ? $(row).attr('class') : ''), - click: function (e) {e.stopPropagation();} - }) - .addClass('oe_form oe_form_container') - .delegate('button.oe_list_edit_row_save', 'click', function () { - self.save_row(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function () { - return self.on_row_keyup.apply(self, arguments); }) - .keydown(function (e) { e.stopPropagation(); }) - .keypress(function (e) { - if (e.which === KEY_RETURN) { - return false; - } - }); - - if (row) { - $new_row.replaceAll(row); - } else if (self.options.editable) { - var $last_child = self.$current.children('tr:last'); - if (self.records.length) { - if (self.options.editable === 'top') { - $new_row.insertBefore( - self.$current.children('[data-id]:first')); - } else { - $new_row.insertAfter( - self.$current.children('[data-id]:last')); + return this.with_event('cancel', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.cancel().pipe(function (attrs) { + if (attrs.id) { + var record = self.records.get(attrs.id); + if (!record) { + // Record removed by third party during edition + return } - } else { - $new_row.prependTo(self.$current); + return self.reload_record(record); } - if ($last_child.is(':not([data-id])')) { - $last_child.remove(); - } - } - self.edition = true; - self.edition_id = record_id; - self.dataset.index = _(self.dataset.ids).indexOf(record_id); - if (self.dataset.index === -1) { - self.dataset.index = null; - } - self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), { - $element: $new_row, - editable_list: self - }); - // put in $.when just in case FormView.on_loaded becomes asynchronous - return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - $new_row.find('> td') - .end() - .find('td:last').removeClass('oe_list_field_cell').end(); - // pad in case of groupby - _(self.columns).each(function (column) { - if (column.meta) { - $new_row.prepend('<td>'); - } + var to_delete = self.records.find(function (r) { + return !r.get('id'); }); - // Add column for the save, if - // there is none in the list - if (!self.options.deletable) { - self.view.pad_columns( - 1, {except: $new_row}); - } - - self.edition_form.do_show(); - }); - }); - }, - handle_onwrite: function (source_record_id) { - var self = this; - var on_write_callback = self.view.fields_view.arch.attrs.on_write; - if (!on_write_callback) { return; } - this.dataset.call(on_write_callback, [source_record_id], function (ids) { - _(ids).each(function (id) { - var record = self.records.get(id); - if (!record) { - // insert after the source record - var index = self.records.indexOf( - self.records.get(source_record_id)) + 1; - record = new instance.web.list.Record({id: id}); - self.records.add(record, {at: index}); - self.dataset.ids.splice(index, 0, id); + if (to_delete) { + self.records.remove(to_delete); } - self.reload_record(record); }); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: + * Executes an action on the view's editor bracketed by a cancellable + * event of the name provided. * - * ``created`` - * Boolean flag indicating whether the record saved was being created - * (``true`` or edited (``false``) - * ``edited_record`` - * The result of saving the record (either the newly created record, - * or the post-edition record), after insertion in the Collection if - * needs be. + * 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 * - * @returns {$.Deferred<{created: Boolean, edited_record: Record}>} + * @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} */ - save_row: function () { - //noinspection JSPotentiallyInvalidConstructorUsage + with_event: function (event_name, event, action) { var self = this; - return this.edition_form - .do_save(null, this.options.editable === 'top') - .pipe(function (result) { - if (result.created && !self.edition_id) { - self.records.add({id: result.result}, - {at: self.options.editable === 'top' ? 0 : null}); - self.edition_id = result.result; - } - var edited_record = self.records.get(self.edition_id); - - return $.when( - self.handle_onwrite(self.edition_id), - self.cancel_pending_edition().then(function () { - $(self).trigger('saved', [self.dataset]); - })).pipe(function () { - return { - created: result.created || false, - edited_record: edited_record - }; - }); + event = event || {}; + this.trigger(event_name + ':before', event); + if (event.cancel) { + return $.Deferred().reject({ + message: _.str.sprintf("Event %s:before cancelled", + event_name)}); + } + return $.when(action.call(this)).then(function () { + self.trigger.apply(self, [event_name + ':after'] + .concat(_.toArray(arguments))); + }); + }, + edition_view: function (editor) { + 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; + }, + handle_onwrite: function (source_record) { + 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) { + return $.when.apply( + null, _(ids).map( + _.bind(self.handle_onwrite_record, self, source_record))); }); }, + handle_onwrite_record: function (source_record, id) { + 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); + }, + prepends_on_create: function () { + return this.editable() === 'top'; + }, + setup_events: function () { + var self = this; + this.editor.$element.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return; } + 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; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return; } + self[method](e); + }); + }, /** - * If the current list is being edited, ensures it's saved + * 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 + * @param {Object} [options] + * @param {String} [options.focus_field] + * @return {*} */ - ensure_saved: function () { - if (this.edition) { - // kinda-hack-ish: if the user has entered data in a field, - // oe_form_dirty will be set on the form so save, otherwise - // discard the current (entirely empty) line - if (this.edition_form.$element.is('.oe_form_dirty')) { - return this.save_row(); + _next: function (next_record, options) { + next_record = next_record || 'succ'; + var self = this; + return this.save_edition().pipe(function (saveInfo) { + if (saveInfo.created) { + return self.start_edition(); + } + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); + }); + }, + keyup_ENTER: function () { + return this._next(); + }, + keyup_ESCAPE: function () { + return this.cancel_edition(); + }, + _text_selection_range: function (el) { + if (el.selectionStart !== undefined) { + return { + start: el.selectionStart, + end: el.selectionEnd + }; + } else if(document.body.createTextRange) { + throw new Error("Implement text range handling for MSIE"); + var sel = document.body.createTextRange(); + if (sel.parentElement() === el) { + } - return this.cancel_pending_edition(); } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.when(); }, - /** - * Cancels the edition of the row for the current dataset index - */ - cancel_edition: function () { - this.cancel_pending_edition(); + _text_cursor: function (el) { + var selection = this._text_selection_range(el); + if (selection.start !== selection.end) { + return null; + } + return selection.start; }, /** - * Edits record currently selected via dataset + * @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 */ - edit_record: function (record_id) { - this.render_row_as_form( - this.$current.find('[data-id=' + record_id + ']')); - $(this).trigger( - 'edit', - [record_id, this.dataset]); - }, - new_record: function () { - this.render_row_as_form(); - }, - render_record: function (record) { - var index = this.records.indexOf(record), - self = this; - // FIXME: context dict should probably be extracted cleanly - return QWeb.render('ListView.row', { - columns: this.columns, - options: this.options, - record: record, - row_parity: (index % 2 === 0) ? 'even' : 'odd', - view: this.view, - render_cell: function () { - return self.render_cell.apply(self, arguments); }, - edited: !!this.edition_form + _key_move_record: function (event, record_direction, is_valid_move) { + if (!this.editor.is_editing('edit')) { return $.when(); } + // FIXME: assumes editable widgets are input-type elements + var index = this._text_cursor(event.target); + // If selecting or not at the start of the input + if (!is_valid_move(event.target, index)) { return $.when(); } + + event.preventDefault(); + var source_field = $(event.target).closest('[data-fieldname]') + .attr('data-fieldname'); + return this._next(record_direction, {focus_field: source_field}); + + }, + keydown_UP: function (e) { + return this._key_move_record(e, 'pred', function (el, index) { + return index === 0; + }); + }, + keydown_DOWN: function (e) { + return this._key_move_record(e, 'succ', function (el, index) { + return index === el.value.length; }); + }, + + 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'); + var index = this._text_cursor(e.target); + if (index !== 0) { return $.when(); } + + 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]]; + } while (!field.$element.is(':visible')); + + // 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'); + var index = this._text_cursor(e.target); + if (index !== e.target.value.length) { return $.when(); } + + 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]]; + } while (!field.$element.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.$element.is(':visible'); }) + .last() + .value(); + // tabbed from last field in form + if (last_field && last_field.$element.has(e.target).length) { + e.preventDefault(); + return this._next(); + } + return $.when(); } }); - - instance.web.ListEditableFormView = instance.web.FormView.extend({ - init: function() { - this._super.apply(this, arguments); - this.rendering_engine = new instance.web.ListEditableRenderingEngine(this); - this.options.initial_mode = "edit"; + + 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', + $buttons: $(), + $pager: $() + }); }, - renderElement: function() {} - }); - - instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ - init: function(view) { - this.view = view; + 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.$element).then( + self.form.proxy('do_hide')); + return $.when(_super, form_ready); }, - set_fields_view: function(fields_view) { - this.fvg = fields_view; + _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; }, - set_tags_registry: function(tags_registry) { - this.tags_registry = tags_registry; + + /** + * + * @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"); }, - set_fields_registry: function(fields_registry) { - this.fields_registry = fields_registry; + _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.$element.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.$element.is(':visible')) { + return false; + } + field.focus(); + // Stop as soon as a field got focused + return true; + }); }, - render_to: function($element) { + edit: function (record, configureField, options) { + // TODO: specify sequence of edit calls var self = this; - - var xml = instance.web.json_node_to_xml(this.fvg.arch); - var $xml = $(xml); - - if (this.view.editable_list.options.selectable) - $("<td>").appendTo($element); - - $xml.children().each(function(i, el) { - var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); - var $td = $("<td>"); - if (modifiers.tree_invisible === true) - $td.hide(); - var tag_name = el.tagName.toLowerCase(); - var w; - if (tag_name === "field") { - var name = $(el).attr("name"); - var key = $(el).attr('widget') || self.fvg.fields[name].type; - var obj = self.view.fields_registry.get_object(key); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - self.view.register_field(w, $(el).attr("name")); - } else { - var obj = self.tags_registry.get_object(tag_name); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - } - w.appendTo($td); - $td.appendTo($element); + var form = self.form; + var loaded = record + ? form.on_record_loaded(_.extend({}, record)) + : form.load_defaults(); + + return loaded.pipe(function () { + return form.do_show({reload: false}); + }).pipe(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 + .do_save(null, this.delegate.prepends_on_create()) + .pipe(function (result) { + var created = result.created && !self.record.id; + if (created) { + self.record.id = result.result; + } + return self.cancel(); + }); + }, + cancel: function () { + var record = this.record; + this.record = null; + if (!this.form.can_be_discarded()) { + return $.Deferred().reject({ + message: "The form's data can not be discarded"}).promise(); + } + this.form.do_hide(); + return $.when(record); + } + }); + + instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ + passtrough_events: instance.web.ListView.Groups.prototype.passtrough_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()) { + return this._super.apply(this, arguments); + } + var record_id = $(event.currentTarget).data('id'); + this.view.start_edition( + record_id ? this.records.get(record_id) : null, { + focus_field: $(event.target).data('field') }); - $(QWeb.render('ListView.row.save')).appendTo($element); }, + /** + * 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; + } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index bd102d867de..ddf4050fa38 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -274,7 +274,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ }, ir_actions_report_xml: function(action, on_closed) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -284,7 +284,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, - complete: $.unblockUI, + complete: instance.web.unblockUI, success: function(){ if (!self.dialog && on_closed) { on_closed(); @@ -841,7 +841,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ } else { self.do_attachement_update(self.dataset, self.model_id); } - $.unblockUI(); + instance.web.unblockUI(); }); }, start: function() { @@ -980,7 +980,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ $e.parent().find('input[type=file]').prop('disabled', true); $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...')); - $.blockUI(); + instance.web.blockUI(); } }, on_attachment_delete: function(e) { @@ -1074,11 +1074,13 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ if (self.view.translatable_fields && self.view.translatable_fields.length) { self.do_load_fields_values(function() { sup.call(self); + // desactivated because it created an exception, plus it does not seem very useful + /* if (field) { var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child'); self.$element.scrollTo($field_input); $field_input.focus(); - } + }*/ }); } else { sup.call(self); @@ -1346,7 +1348,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { if (typeof(node) === 'string') { return sindent + node; } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) { - throw("Node a json node"); + throw new Error( + _.str.sprintf("Node [%s] is not a JSONified XML node", + JSON.stringify(node))); } for (var attr in node.attrs) { var vattr = node.attrs[attr]; |
