X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fweb%2Fstatic%2Fsrc%2Fjs%2Fview_form.js;h=c6295ea7298ff67e24a34b3c26f299ebbf182d44;hb=2b1bd31f7449fc88f1e18eb64384c1b81b85fe28;hp=b8ea69d1c3561416f92420de0527f5c0d15c467c;hpb=45fc9bba23f03479e8ada9276c830d4a8fa9a308;p=odoo%2Fodoo.git diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index b8ea69d..c6295ea 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -33,7 +33,7 @@ instance.web.form.FieldManagerMixin = { }; instance.web.views.add('form', 'instance.web.FormView'); -instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form.FieldManagerMixin, { +instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -78,13 +78,17 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); + this.__clicked_inside = false; + this.__blur_timeout = null; this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template }, destroy: function() { _.each(this.get_widgets(), function(w) { + w.off('focused blurred'); w.destroy(); }); + this.$element.off('.formBlur'); this._super(); }, on_loaded: function(data) { @@ -106,6 +110,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.rendering_engine.render_to($dest); } + this.$element.on('mousedown.formBlur', function () { + self.__clicked_inside = true; + }); this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); if (this.options.$buttons) { @@ -197,6 +204,30 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.translatable_fields = []; this.$element.find('.oe_form_container').empty(); }, + + widgetFocused: function() { + // Clear click flag if used to focus a widget + this.__clicked_inside = false; + if (this.__blur_timeout) { + clearTimeout(this.__blur_timeout); + this.__blur_timeout = null; + } + }, + widgetBlurred: function() { + if (this.__clicked_inside) { + // clicked in an other section of the form (than the currently + // focused widget) => just ignore the blurring entirely? + this.__clicked_inside = false; + return; + } + var self = this; + // clear timeout, if any + this.widgetFocused(); + this.__blur_timeout = setTimeout(function () { + self.trigger('blurred'); + }, 0); + }, + do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -208,8 +239,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } } }, - do_show: function () { + do_show: function (options) { var self = this; + options = options || {}; if (this.sidebar) { this.sidebar.$element.show(); } @@ -233,6 +265,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }).pipe(self.on_record_loaded); } result.pipe(function() { + if (options.editable) { + self.set({mode: "edit"}); + } self.$element.css('visibility', 'visible'); }); return result; @@ -469,13 +504,13 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return self.on_processed_onchange(response, processed); } catch(e) { console.error(e); + instance.webclient.crashmanager.on_javascript_exception(e); return $.Deferred().reject(); } }); }, - on_processed_onchange: function(response, processed) { + on_processed_onchange: function(result, processed) { try { - var result = response; if (result.value) { for (var f in result.value) { if (!result.value.hasOwnProperty(f)) { continue; } @@ -505,6 +540,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } if (result.domain) { function edit_domain(node) { + if (typeof node !== "object") { + return; + } var new_domain = result.domain[node.attrs.name]; if (new_domain) { node.attrs.domain = new_domain; @@ -516,6 +554,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return $.Deferred().resolve(); } catch(e) { console.error(e); + instance.webclient.crashmanager.on_javascript_exception(e); return $.Deferred().reject(); } }, @@ -548,6 +587,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. on_button_cancel: function(event) { if (this.can_be_discarded()) { this.set({mode: "view"}); + this.on_record_loaded(this.datarecord); } return false; }, @@ -617,7 +657,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. * record or saving an existing one depending on whether the record * already has an id property. * - * @param {Function} success callback on save success + * @param {Function} [success] callback on save success * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) */ do_save: function(success, prepend_on_create) { @@ -643,6 +683,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } if (form_invalid) { self.set({'display_invalid_fields': true}); + for (var f in self.fields) { + self.fields[f]._check_css_flags(); + } first_invalid_field.focus(); self.on_invalid(); return $.Deferred().reject(); @@ -688,7 +731,8 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { - return $.when(r).then(success); }, null); + return r; }) + .then(success); } }, /** @@ -721,9 +765,10 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } - //instance.log("The record has been created with id #" + this.datarecord.id); - this.reload(); - return $.when(_.extend(r, {created: true})).then(success); + //openerp.log("The record has been created with id #" + this.datarecord.id); + return $.when(this.reload()).pipe(function () { + return _.extend(r, {created: true}); }) + .then(success); } }, on_action: function (action) { @@ -867,6 +912,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); + + field.on('focused', null, this.proxy('widgetFocused')) + .on('blurred', null, this.proxy('widgetBlurred')); if (this.get_field(name).translate) { this.translatable_fields.push(field); } @@ -885,7 +933,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. is_create_mode: function() { return !this.datarecord.id; }, -})); +}); /** * Interface to be implemented by rendering engines for the form view. @@ -907,6 +955,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }, set_fields_view: function(fvg) { this.fvg = fvg; + this.version = parseFloat(this.fvg.arch.attrs.version); + if (isNaN(this.version)) { + this.version = 6.1; + } }, set_tags_registry: function(tags_registry) { this.tags_registry = tags_registry; @@ -914,6 +966,18 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt set_fields_registry: function(fields_registry) { this.fields_registry = fields_registry; }, + // Backward compatibility tools, current default version: v6.1 + process_version: function() { + if (this.version < 7.0) { + this.$form.find('form:first').wrapInner(''); + this.$form.find('page').each(function() { + if (!$(this).parents('field').length) { + $(this).wrapInner(''); + } + }); + } + selector = 'form[version!="7.0"] page,form[version!="7.0"]'; + }, render_to: function($target) { var self = this; this.$target = $target; @@ -923,6 +987,8 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt var xml = instance.web.json_node_to_xml(this.fvg.arch); this.$form = $('
' + xml + '
'); + this.process_version(); + this.fields_to_init = []; this.tags_to_init = []; this.labels = {}; @@ -1414,7 +1480,7 @@ instance.web.form.InvisibilityChangerMixin = { }, }; -instance.web.form.InvisibilityChanger = instance.web.Class.extend(_.extend({}, instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { +instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { init: function(parent, field_manager, invisible_domain, $element) { this.setParent(parent); instance.web.PropertiesMixin.init.call(this); @@ -1422,9 +1488,9 @@ instance.web.form.InvisibilityChanger = instance.web.Class.extend(_.extend({}, i this.$element = $element; this.start(); }, -})); +}); -instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance.web.form.InvisibilityChangerMixin, { +instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, { /** * @constructs instance.web.form.FormWidget * @extends instance.web.Widget @@ -1449,6 +1515,18 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. $.fn.tipsy.clear(); this._super.apply(this, arguments); }, + /** + * Sets up blur/focus forwarding from DOM elements to a widget (`this`) + * + * @param {jQuery} $e jQuery object of elements to bind focus/blur on + */ + setupFocus: function ($e) { + var self = this; + $e.on({ + focus: function () { self.trigger('focused'); }, + blur: function () { self.trigger('blurred'); } + }); + }, process_modifiers: function() { var compute_domain = instance.web.form.compute_domain; var to_set = {}; @@ -1530,7 +1608,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. } return final_domain; } -})); +}); instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ template: 'WidgetButton', @@ -1546,10 +1624,12 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.click(this.on_click); + var $button = this.$element.find('button'); + $button.click(this.on_click); if (this.node.attrs.help || instance.connection.debug) { this.do_attach_tooltip(); } + this.setupFocus($button); }, on_click: function() { var self = this; @@ -1628,7 +1708,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ * - changed_value: triggered to inform the view to check on_changes * */ -instance.web.form.FieldMixin = { +instance.web.form.FieldInterface = { /** * Constructor takes 2 arguments: * - field_manager: Implements FieldManagerMixin @@ -1692,7 +1772,7 @@ instance.web.form.FieldMixin = { }; /** - * Abstract class for classes implementing FieldMixin. + * Abstract class for classes implementing FieldInterface. * * Properties: * - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending @@ -1702,7 +1782,7 @@ instance.web.form.FieldMixin = { * a 'changed_value' event that inform the view to trigger on_changes. * */ -instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({}, instance.web.form.FieldMixin, { +instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, { /** * @constructs instance.web.form.AbstractField * @extends instance.web.form.FormWidget @@ -1775,7 +1855,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ return this.get('value'); }, is_valid: function() { - return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); + return this.is_syntax_valid() && !(this.get('required') && this.is_false()); }, is_syntax_valid: function() { return true; @@ -1820,7 +1900,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ set_input_id: function(id) { this.id_for_label = id; }, -})); +}); /** * A mixin to apply on any field that has to completely re-render when its readonly state @@ -1859,7 +1939,7 @@ instance.web.form.ReinitializeFieldMixin = { render_value: function() {}, }; -instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldChar', widget_class: 'oe_form_field_char', init: function (field_manager, node) { @@ -1868,9 +1948,11 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} }, initialize_content: function() { var self = this; - this.$element.find('input').change(function() { - self.set({'value': instance.web.parse_value(self.$element.find('input').val(), self)}); + var $input = this.$element.find('input'); + $input.change(function() { + self.set({'value': instance.web.parse_value($input.val(), self)}); }); + this.setupFocus($input); }, set_value: function(value_) { this._super(value_); @@ -1899,12 +1981,12 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, focus: function() { this.delay_focus(this.$element.find('input:first')); } -})); +}); instance.web.form.FieldID = instance.web.form.FieldChar.extend({ @@ -1914,7 +1996,9 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ template: 'FieldEmail', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$element.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -1938,7 +2022,9 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ template: 'FieldUrl', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$element.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -1971,7 +2057,7 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ this._super(field_manager, node); this.set({'value': 0}); if (this.node.attrs.digits) { - this.digits = py.eval(node.attrs.digits); + this.digits = this.node.attrs.digits; } else { this.digits = this.field.digits; } @@ -1999,6 +2085,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ this.$input_picker = this.$element.find('input.oe_datepicker_container'); this.$input.change(this.on_change); this.picker({ + onClose: this.on_picker_select, onSelect: this.on_picker_select, changeMonth: true, changeYear: true, @@ -2006,12 +2093,14 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ showButtonPanel: true }); this.$element.find('img.oe_datepicker_trigger').click(function() { - if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { - self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); - self.$input_picker.show(); - self.picker('show'); - self.$input_picker.hide(); + if (self.get("effective_readonly") || self.picker('widget').is(':visible')) { + self.$input.focus(); + return; } + self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.$input_picker.show(); + self.picker('show'); + self.$input_picker.hide(); }); this.set_readonly(false); this.set({'value': false}); @@ -2021,7 +2110,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, on_picker_select: function(text, instance_) { var date = this.picker('getDate'); - this.$input.val(date ? this.format_client(date) : '').change(); + this.$input + .val(date ? this.format_client(date) : '') + .change() + .focus(); }, set_value: function(value_) { this.set({'value': value_}); @@ -2070,7 +2162,7 @@ instance.web.DateWidget = instance.web.DateTimeWidget.extend({ type_of_date: "date" }); -instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: "FieldDatetime", build_widget: function() { return new instance.web.DateTimeWidget(this); @@ -2088,6 +2180,7 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten this.set({'value': this.datewidget.get_value()}); }, this)); this.datewidget.appendTo(this.$element); + this.setupFocus(this.datewidget.$input); } }, set_value: function(value_) { @@ -2108,13 +2201,13 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, focus: function() { if (this.datewidget && this.datewidget.$input) this.delay_focus(this.datewidget.$input); } -})); +}); instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ template: "FieldDate", @@ -2123,7 +2216,7 @@ instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ } }); -instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldText', initialize_content: function() { this.$textarea = this.$element.find('textarea'); @@ -2134,6 +2227,7 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} } else { this.$textarea.attr('disabled', 'disabled'); } + this.setupFocus(this.$textarea); }, set_value: function(value_) { this._super.apply(this, arguments); @@ -2158,7 +2252,7 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, focus: function($element) { this.delay_focus(this.$textarea); @@ -2183,13 +2277,14 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} $div.remove(); $input.height(new_height); }, -})); +}); instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { this._super.apply(this, arguments); this.$checkbox = $("input", this.$element); + this.setupFocus(this.$checkbox); this.$element.click(_.bind(function() { this.set({'value': this.$checkbox.is(':checked')}); }, this)); @@ -2232,7 +2327,7 @@ instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({ // to replace view editor }); -instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldSelection', init: function(field_manager, node) { var self = this; @@ -2258,7 +2353,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte // changing the selected value), takes the action as validating the // row var ischanging = false; - this.$element.find('select') + var $select = this.$element.find('select') .change(_.bind(function() { this.set({'value': this.values[this.$element.find('select')[0].selectedIndex][0]}); }, this)) @@ -2269,6 +2364,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte e.stopPropagation(); ischanging = false; }); + this.setupFocus($select); }, set_value: function(value_) { value_ = value_ === null ? false : value_; @@ -2300,7 +2396,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte focus: function() { this.delay_focus(this.$element.find('select:first')); } -})); +}); // jquery autocomplete tweak to allow html (function() { @@ -2385,7 +2481,7 @@ instance.web.form.CompletionFieldMixin = { } // create... values.push({label: _t("   Create and Edit..."), action: function() { - self._search_create_popup("form", undefined, {"default_name": search_val}); + self._search_create_popup("form", undefined, {}); }}); return values; @@ -2427,6 +2523,7 @@ instance.web.form.CompletionFieldMixin = { ); pop.on_select_elements.add(function(element_ids) { self.add_id(element_ids[0]); + self.focus(); }); }, /** @@ -2435,8 +2532,7 @@ instance.web.form.CompletionFieldMixin = { add_id: function(id) {}, }; -instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, - instance.web.form.CompletionFieldMixin, { +instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2One", init: function(field_manager, node) { this._super(field_manager, node); @@ -2477,6 +2573,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.$follow_button.click(function() { if (!self.get('value')) { + self.focus(); return; } var pop = new instance.web.form.FormOpenPopup(self.view); @@ -2491,6 +2588,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten pop.on_write_completed.add_last(function() { self.display_value = {}; self.render_value(); + self.focus(); }); }); @@ -2505,13 +2603,13 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.$drop_down.click(function() { if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); + self.$input.focus(); } else { if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); } else { self.$input.autocomplete("search"); } - self.$input.focus(); } }); var tip_def = $.Deferred(); @@ -2519,6 +2617,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten var tip_delay = 200; var tip_duration = 3000; var anyoneLoosesFocus = function() { + var used = false; if (self.floating) { if (self.last_search.length > 0) { if (self.last_search[0][0] != self.get("value")) { @@ -2526,13 +2625,17 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; self.set({value: self.last_search[0][0]}); } else { + used = true; self.render_value(); } } else { + used = true; self.set({value: false}); + self.render_value(); } + self.floating = false; } - if (! self.get("value")) { + if (used) { tip_def.reject(); untip_def.reject(); tip_def = $.Deferred(); @@ -2579,7 +2682,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten e.preventDefault(); }, html: true, - close: anyoneLoosesFocus, + // disabled to solve a bug, but may cause others + //close: anyoneLoosesFocus, minLength: 0, delay: 0 }); @@ -2592,6 +2696,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten } isSelecting = false; }); + this.setupFocus(this.$input.add(this.$follow_button)); }, render_value: function(no_recurse) { @@ -2656,7 +2761,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten focus: function () { this.delay_focus(this.$input); } -})); +}); /* # Values: (0, 0, { fields }) create @@ -2717,6 +2822,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); + this.$element.addClass('oe_form_field_one2many'); var self = this; @@ -2724,7 +2830,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.child_name = this.name; - //this.dataset.child_name = this.dataset.on_change.add_last(function() { self.trigger_on_change(); }); @@ -2756,6 +2861,13 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ modes = !!modes ? modes.split(",") : ["tree"]; var views = []; _.each(modes, function(mode) { + if (! _.include(["list", "tree", "graph", "kanban"], mode)) { + try { + throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode)); + } catch(e) { + instance.webclient.crashmanager.on_javascript_exception(e) + } + } var view = { view_id: false, view_type: mode == "tree" ? "list" : mode, @@ -2766,9 +2878,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ } if(view.view_type === "list") { view.options.selectable = self.multi_selection; + view.options.sortable = false; if (self.get("effective_readonly")) { view.options.addable = null; view.options.deletable = null; + view.options.reorderable = false; } } else if (view.view_type === "form") { if (self.get("effective_readonly")) { @@ -2789,6 +2903,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.views = views; this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {}); + this.viewmanager.$element.addClass("oe_view_manager_one2many"); this.viewmanager.o2m = self; var once = $.Deferred().then(function() { self.init_form_last_update.resolve(); @@ -2961,13 +3076,15 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ if (!this.viewmanager.views[this.viewmanager.active_view]) return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - for (var f in view.fields) { - f = view.fields[f]; - if (!f.is_valid()) { - return false; - } - } + switch (this.viewmanager.active_view) { + case 'form': + return _(view.fields).chain() + .invoke('is_valid') + .all(_.identity) + .value(); + break; + case 'list': + return view.is_valid(); } return true; }, @@ -3027,6 +3144,46 @@ instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ instance.web.form.One2ManyListView = instance.web.ListView.extend({ _template: 'One2Many.listview', + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, _.extend(options || {}, { + ListType: instance.web.form.One2ManyList + })); + }, + is_valid: function () { + var form; + // A list not being edited is always valid + if (!(form = this.first_edition_form())) { + return true; + } + // 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; + } + + // Otherwise validate internal form + return _(form.fields).chain() + .invoke(function () { + this._check_css_flag(); + return this.is_valid(); + }) + .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) { this._super.apply(this, arguments); @@ -3081,9 +3238,55 @@ 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 () { var self = this; - var def = $.Deferred().then(callback).then(function() {self.o2m.view.reload();}); - return this._super(name, id, _.bind(def.resolve, def)); + return this._super.apply(this, arguments).then(function () { + // Replace the "Save Row" button with "Cancel Edition" + self.edition_form.$element + .undelegate('button.oe-edit-row-save', 'click') + .delegate('button.oe-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(); + } + }); + }); + }, + on_row_keyup: function (e) { + if (e.which === this.KEY_RETURN) { + this.__return_blur = true; + } + this._super(e); } }); @@ -3112,8 +3315,7 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, - instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2ManyTags", init: function() { this._super.apply(this, arguments); @@ -3237,7 +3439,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. add_id: function(id) { this.set({'value': _.uniq(this.get('value').concat([id]))}); }, -})); +}); /* * TODO niv: clean those deferred stuff, it could be better @@ -3253,6 +3455,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); + this.$element.addClass('oe_form_field_many2many'); var self = this; @@ -3261,7 +3464,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ this.dataset.on_unlink.add_last(function(ids) { self.dataset_changed(); }); - + this.is_setted.then(function() { self.load_view(); }); @@ -3274,7 +3477,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }); }); }); - }) + }); }, set_value: function(value_) { value_ = value_ || []; @@ -3287,12 +3490,21 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ self.reload_content(); this.is_setted.resolve(); }, + get_value: function() { + return [commands.replace_with(this.get('value'))]; + }, + + is_false: function () { + return _(this.dataset.ids).isEmpty(); + }, load_view: function() { var self = this; this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { 'addable': self.get("effective_readonly") ? null : _t("Add"), 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, + 'sortable': false, + 'reorderable': false, }); var embedded = (this.field.views || {}).tree; if (embedded) { @@ -3316,7 +3528,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }); }, dataset_changed: function() { - this.set({'value': [commands.replace_with(this.dataset.ids)]}); + this.set({'value': this.dataset.ids}); }, }); @@ -3366,7 +3578,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in } }); -instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, { +instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { disable_utility_classes: true, init: function(field_manager, node) { this._super(field_manager, node); @@ -3489,7 +3701,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( add_id: function(id) { this.quick_create.add_id(id); }, -})); +}); function m2m_kanban_lazy_init() { if (instance.web.form.Many2ManyKanbanView) @@ -3771,7 +3983,8 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.view_list = new instance.web.form.SelectCreateListView(self, self.dataset, false, _.extend({'deletable': false, - 'selectable': !self.options.disable_multiple_selection + 'selectable': !self.options.disable_multiple_selection, + 'read_only': true, }, self.options.list_view_options || {})); self.view_list.popup = self; self.view_list.appendTo($(".oe-select-create-popup-view-list", self.$element)).pipe(function() { @@ -3841,7 +4054,7 @@ instance.web.form.SelectCreateListView = instance.web.ListView.extend({ } }); -instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldReference', init: function(field_manager, node) { this._super(field_manager, node); @@ -3868,6 +4081,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte } }, initialize_content: function() { + var self = this; this.selection = new instance.web.form.FieldSelection(this, { attrs: { name: 'selection' }}); @@ -3877,6 +4091,9 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte this.selection.$element = $(".oe_form_view_reference_selection", this.$element); this.selection.renderElement(); this.selection.start(); + this.selection + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); this.m2o = new instance.web.form.FieldMany2One(this, { attrs: { name: 'm2o' @@ -3887,6 +4104,9 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); this.m2o.renderElement(); this.m2o.start(); + this.m2o + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); }, is_false: function() { return typeof(this.get_value()) !== 'string'; @@ -3933,17 +4153,26 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte } throw Exception("Should not happen"); }, -})); +}); -instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { init: function(field_manager, node) { + var self = this; this._super(field_manager, node); - this.iframe = this.element_id + '_iframe'; this.binary_value = false; + this.fileupload_id = _.uniqueId('oe_fileupload'); + $(window).on(this.fileupload_id, function() { + var args = [].slice.call(arguments).slice(1); + self.on_file_uploaded.apply(self, args); + }); + }, + stop: function() { + $(window).off(this.fileupload_id); + this._super.apply(this, arguments); }, initialize_content: function() { this.$element.find('input.oe-binary-file').change(this.on_file_change); - this.$element.find('button.oe-binary-file-save').click(this.on_save_as); + this.$element.find('button.oe_binary_file_save').click(this.on_save_as); this.$element.find('.oe-binary-file-clear').click(this.on_clear); }, human_filesize : function(size) { @@ -3959,8 +4188,8 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper // http://www.html5rocks.com/tutorials/file/dndfiles/ // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler - window[this.iframe] = this.on_file_uploaded; - if ($(e.target).val() != '') { + + if ($(e.target).val() !== '') { this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id); this.$element.find('form.oe-binary-form').submit(); this.$element.find('.oe-binary-progress').show(); @@ -3968,12 +4197,12 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( } }, on_file_uploaded: function(size, name, content_type, file_base64) { - delete(window[this.iframe]); if (size === false) { this.do_warn("File Upload", "There was a problem while uploading your file"); // TODO: use openerp web crashmanager console.warn("Error while uploading file : ", name); } else { + this.filename = name; this.on_file_uploaded_and_valid.apply(this, arguments); } this.$element.find('.oe-binary-progress').hide(); @@ -3981,20 +4210,40 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { }, - on_save_as: function() { - $.blockUI(); - this.session.get_file({ - url: '/web/binary/saveas_ajax', - data: {data: JSON.stringify({ - model: this.view.dataset.model, - id: (this.view.datarecord.id || ''), - field: this.name, - filename_field: (this.node.attrs.filename || ''), - context: this.view.dataset.get_context() - })}, - complete: $.unblockUI, - error: instance.webclient.crashmanager.on_rpc_error - }); + on_save_as: function(ev) { + var value = this.get('value'); + if (!value) { + this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !")); + ev.stopPropagation(); + } else if (this._dirty_flag) { + var link = this.$('.oe_binary_file_save_data')[0]; + link.download = this.filename || "download.bin"; // Works on only on Google Chrome + //link.target = '_blank'; + link.href = "data:application/octet-stream;base64," + value; + } else { + $.blockUI(); + this.session.get_file({ + url: '/web/binary/saveas_ajax', + data: {data: JSON.stringify({ + model: this.view.dataset.model, + id: (this.view.datarecord.id || ''), + field: this.name, + filename_field: (this.node.attrs.filename || ''), + context: this.view.dataset.get_context() + })}, + complete: $.unblockUI, + error: instance.webclient.crashmanager.on_rpc_error + }); + ev.stopPropagation(); + return false; + } + }, + set_filename: function(value) { + var filename = this.node.attrs.filename; + if (this.view.fields[filename]) { + this.view.fields[filename].set_value(value); + this.view.fields[filename].on_ui_change(); + } }, on_clear: function() { if (this.get('value') !== false) { @@ -4003,7 +4252,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( } return false; } -})); +}); instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryFile', @@ -4062,14 +4311,6 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryImage', - initialize_content: function() { - this._super(); - if (!this.get("effective_readonly")) { - this.$element.find('.oe_form_field_image_controls').show(); - } else { - this.$element.find('.oe_form_field_image_controls').hide(); - } - }, set_value: function(value_) { this._super.apply(this, arguments); this.render_value(); @@ -4224,7 +4465,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ /** * Registry of form fields, called by :js:`instance.web.FormView`. * - * All referenced classes must implement FieldMixin. Those represent the classes whose instances + * All referenced classes must implement FieldInterface. Those represent the classes whose instances * will substitute to the tags as defined in OpenERP's views. */ instance.web.form.widgets = new instance.web.Registry({