diff options
| author | Amit Patel (OpenERP) <apa@tinyerp.com> | 2012-07-27 10:58:09 +0530 |
|---|---|---|
| committer | Amit Patel (OpenERP) <apa@tinyerp.com> | 2012-07-27 10:58:09 +0530 |
| commit | 5aee509a3b7585e32e1273e1925d1d25762e89fe (patch) | |
| tree | d6b494809e2d1eab9e8a9553fe4984eba9b79cb3 /addons/web/static/src/js | |
| parent | ff71d6d1ade22e9e1299f81a6a05043466de26ba (diff) | |
| parent | 9128e37231194592b6468ffa7922bbb39b5d7a33 (diff) | |
[MERGE]
bzr revid: apa@tinyerp.com-20120727052809-oezzaxn0qikgtkli
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 81 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 12 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 34 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 29 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 168 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 33 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 188 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 83 |
9 files changed, 442 insertions, 190 deletions
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 71010ef6d74..a1759397e21 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -257,7 +257,7 @@ 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); } @@ -276,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'); @@ -287,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")); @@ -354,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 @@ -387,8 +387,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ var fields = $(form).serializeArray(); self.rpc("/web/database/create", {'fields': fields}, function(result) { var form_obj = self.to_object(fields); - self.getParent().do_login( form_obj['db_name'], 'admin', form_obj['create_admin_pwd']); - self.destroy(); + var client_action = { + type: 'ir.actions.client', + tag: 'login', + params: { + 'db': form_obj['db_name'], + 'login': 'admin', + 'password': form_obj['create_admin_pwd'], + }, + }; + self.do_action(client_action); }); }, @@ -473,19 +481,22 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }); }, do_exit: function () { + this.do_action("login"); } }); +instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager"); instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, _db_list: null, - init: function(parent) { + init: function(parent, params) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; this.selected_db = null; this.selected_login = null; + this.params = params; if (this.has_local_storage && this.remember_credentials) { this.selected_db = localStorage.getItem('last_db_login_success'); @@ -495,26 +506,17 @@ instance.web.Login = instance.web.Widget.extend({ } } }, - open_db_manager: function(){ - var self = this; - self.$element.find('.oe_login_bottom').hide(); - self.$element.find('.oe_login_pane').hide(); - self.databasemanager = new instance.web.DatabaseManager(self); - self.databasemanager.appendTo(self.$element); - self.databasemanager.do_exit.add_last(function() { - self.databasemanager.destroy(); - self.$element.find('.oe_login_bottom').show(); - self.$element.find('.oe_login_pane').show(); - self.load_db_list(true).then(self.on_db_list_loaded); - }); - }, start: function() { var self = this; self.$element.find("form").submit(self.on_submit); self.$element.find('.oe_login_manage_db').click(function() { - self.open_db_manager(); + self.do_action("database_manager"); + }); + return self.load_db_list().then(self.on_db_list_loaded).then(function() { + if(self.params) { + self.do_login(self.params.db, self.params.login, self.params.password); + } }); - return self.load_db_list().then(self.on_db_list_loaded); }, load_db_list: function (force) { var d = $.when(), self = this; @@ -535,7 +537,7 @@ instance.web.Login = instance.web.Widget.extend({ var dbdiv = this.$element.find('div.oe_login_dbpane'); this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})); if(list.length === 0) { - self.open_db_manager(); + this.do_action("database_manager"); } else if(list && list.length === 1) { dbdiv.hide(); } else { @@ -582,13 +584,14 @@ instance.web.Login = instance.web.Widget.extend({ localStorage.setItem('last_password_login_success', ''); } } - self.trigger("login"); + self.trigger('login_successful'); },function () { self.$(".oe_login_pane").fadeIn("fast"); self.$element.addClass("oe_login_invalid"); }); } }); +instance.web.client_actions.add("login", "instance.web.Login"); instance.web.Menu = instance.web.Widget.extend({ template: 'Menu', @@ -864,7 +867,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")}); }); }, }); @@ -893,7 +896,7 @@ instance.web.Client = instance.web.Widget.extend({ this.$element.on('click', '.oe_dropdown_toggle', function(ev) { ev.preventDefault(); var $toggle = $(this); - var $menu = $toggle.find('.oe_dropdown_menu'); + var $menu = $toggle.parent().find('.oe_dropdown_menu'); var state = $menu.is('.oe_opened'); setTimeout(function() { // Do not alter propagation @@ -903,7 +906,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); } @@ -964,19 +967,15 @@ instance.web.WebClient = instance.web.Client.extend({ data: {debug: file + ':' + line} }); }; - // TODO: deprecate and use login client action - self.login = new instance.web.Login(self); - self.login.on("login",self,self.show_application); }, show_login: function() { - var self = this; - self.$('.oe_topbar').hide(); - self.login.appendTo(self.$element); + this.$('.oe_topbar').hide(); + this.action_manager.do_action("login"); + this.action_manager.inner_widget.on('login_successful', this, this.show_application); }, 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')); self.menu.on('menu_click', this, this.on_menu_action); @@ -1057,6 +1056,7 @@ instance.web.WebClient = instance.web.Client.extend({ }, do_push_state: function(state) { this.set_title(state.title); + delete state.title; var url = '#' + $.param(state); this._current_state = _.clone(state); $.bbq.pushState(url); @@ -1083,10 +1083,13 @@ instance.web.WebClient = instance.web.Client.extend({ } }, set_content_full_screen: function(fullscreen) { - if (fullscreen) + if (fullscreen) { $(".oe_webclient", this.$element).addClass("oe_content_full_screen"); - else + $("body").css({'overflow-y':'hidden'}); + } else { $(".oe_webclient", this.$element).removeClass("oe_content_full_screen"); + $("body").css({'overflow-y':'scroll'}); + } } }); diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 1ed4c4788e3..a092b670ef1 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -378,7 +378,8 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(map, options) { + options = options || {}; var self = this; var changed = false; _.each(map, function(val, key) { @@ -387,10 +388,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { return; changed = true; self.__getterSetterInternalMap[key] = val; - self.trigger("change:" + key, self, { - oldValue: tmp, - newValue: val - }); + if (! options.silent) + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); }); if (changed) self.trigger("change", self); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index a5a62b9bcc1..24441dc509f 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -550,7 +550,39 @@ $.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 = '<div class="oe_blockui_spin" style="height: 50px">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; + $.blockUI.spinners = []; +} +instance.web.blockUI = function() { + var tmp = $.blockUI.apply($, arguments); + var target = $(".oe_blockui_spin")[0]; + var opts = { + lines: 13, // The number of lines to draw + length: 7, // The length of each line + width: 4, // The line thickness + radius: 10, // The radius of the inner circle + rotate: 0, // The rotation offset + color: '#FFF', // #rgb or #rrggbb + speed: 1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: false, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: 'auto', // Top position relative to parent in px + left: 'auto' // Left position relative to parent in px + }; + var spinner = new Spinner(opts).spin(target); + $.blockUI.spinners.push(spinner); + return tmp; +} +instance.web.unblockUI = function() { + _.each($.blockUI.spinners, function(el) { + el.stop(); + }); + 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 504510ef133..c3e7ad72dcb 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) { @@ -510,7 +516,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.$element.addClass('oe_focused'); }, childBlurred: function () { - this.$element.removeClass('oe_focused'); + var val = this.$element.val(); + this.$element.val(''); + var complete = this.$element.data('autocomplete'); + if ((val && complete.term === undefined) || complete.previous !== undefined) { + throw new Error("new jquery.ui version altering implementation" + + " details relied on"); + } + delete complete.term; + this.$element.removeClass('oe_focused') + .trigger('blur'); }, /** * @@ -1475,7 +1490,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; } diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 677b7cf7c4e..7f802be2100 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -33,6 +33,11 @@ instance.web.form.FieldManagerMixin = { }; instance.web.views.add('form', 'instance.web.FormView'); +/** + * Properties: + * - actual_mode: always "view", "edit" or "create". Read-only property. Determines + * the mode used by the view. + */ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { /** * Indicates that this view is not searchable, and thus that no search @@ -55,6 +60,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { + var self = this; this._super(parent); this.set_default_options(options); this.dataset = dataset; @@ -82,6 +88,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.__blur_timeout = null; this.rendering_engine = new instance.web.form.FormRenderingEngine(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template + self.set({actual_mode: self.options.initial_mode}); + this.has_been_loaded.then(function() { + self.on("change:actual_mode", self, self.check_actual_mode); + self.check_actual_mode(); + self.on("change:actual_mode", self, self.init_pager); + self.init_pager(); + }); }, destroy: function() { _.each(this.get_widgets(), function(w) { @@ -125,17 +138,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.$buttons.on('click','.oe_form_button_save',this.on_button_save); this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel); - this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); - if (this.options.$pager) { - this.$pager.appendTo(this.options.$pager); - } else { - this.$element.find('.oe_form_pager').replaceWith(this.$pager); - } - this.$pager.on('click','a[data-pager-action]',function() { - var action = $(this).data('pager-action'); - self.on_pager_action(action); - }); - this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); @@ -149,8 +151,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } }, ]); } - this.on("change:mode", this, this.switch_mode); - this.set({mode: this.options.initial_mode}); + + // Add bounce effect on button 'Edit' when click on readonly page view. + this.$element.find(".oe_form_field, .oe_form_group_cell").on('click', function (e) { + if(self.get("actual_mode") == "view") { + var $button = self.options.$buttons.find(".oe_form_button_edit"); + $button.wrap('<div>').css('margin-right','4px').addClass('oe_left oe_bounce'); + } + }); + this.has_been_loaded.resolve(); return $.when(); }, @@ -275,7 +284,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } return shown.pipe(function() { if (options.editable) { - self.set({mode: "edit"}); + self.to_edit_mode(); } self.$element.css('visibility', 'visible'); }); @@ -300,6 +309,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return $.Deferred().reject(); } this.datarecord = record; + this._actualize_mode(); this.set({ 'title' : record.id ? record.name : "New record" }); if (this.qweb) { @@ -377,6 +387,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.reload(); } }, + init_pager: function() { + var self = this; + if (this.$pager) + this.$pager.remove(); + if (this.get("actual_mode") === "create") + return; + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$element.find('.oe_form_pager').replaceWith(this.$pager); + } + this.$pager.on('click','a[data-pager-action]',function() { + var action = $(this).data('pager-action'); + self.on_pager_action(action); + }); + this.do_update_pager(); + }, do_update_pager: function(hide_index) { var index = hide_index ? '-' : this.dataset.index + 1; this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end() @@ -576,9 +604,32 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return $.Deferred().reject(); } }, - switch_mode: function() { + /** + * Ask the view to switch to view mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_view_mode: function() { + this._actualize_mode("view"); + }, + /** + * Ask the view to switch to edit mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_edit_mode: function() { + this._actualize_mode("edit"); + }, + /** + * Reactualize actual_mode. + */ + _actualize_mode: function(switch_to) { + var mode = switch_to || this.get("actual_mode"); + if (! this.datarecord.id) + mode = "create"; + this.set({actual_mode: mode}); + }, + check_actual_mode: function(source, options) { var self = this; - if(this.get("mode") == "view") { + if(this.get("actual_mode") === "view") { self.$element.removeClass('oe_form_editable').addClass('oe_form_readonly'); self.$buttons.find('.oe_form_buttons_edit').hide(); self.$buttons.find('.oe_form_buttons_view').show(); @@ -596,11 +647,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM }); var fields_order = self.fields_order.slice(0); if (self.default_focus_field) { - fields_order.unshift(self.default_focus_field); + fields_order.unshift(self.default_focus_field.name); } for (var i = 0; i < fields_order.length; i += 1) { var field = self.fields[fields_order[i]]; - if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.focus() !== false) { + if (!field.get('effective_invisible') && !field.get('effective_readonly')) { + field.focus(); break; } } @@ -609,19 +661,19 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM on_button_save: function() { var self = this; return this.do_save().then(function(result) { - self.set({mode: "view"}); + self.to_view_mode(); }); }, on_button_cancel: function(event) { if (this.can_be_discarded()) { - this.set({mode: "view"}); + this.to_view_mode(); this.on_record_loaded(this.datarecord); } return false; }, on_button_new: function() { var self = this; - this.set({mode: "edit"}); + this.to_edit_mode(); return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { return self.load_defaults(); @@ -629,7 +681,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM }); }, on_button_edit: function() { - return this.set({mode: "edit"}); + return this.to_edit_mode(); }, on_button_create: function() { this.dataset.index = null; @@ -642,7 +694,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { return self.on_created({ result : new_id }); }).then(function() { - return self.set({mode: "edit"}); + return self.to_edit_mode(); }).then(function() { def.resolve(); }); @@ -935,7 +987,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); - if (field.node.attrs.default_focus == '1') { + if (JSON.parse(field.node.attrs.default_focus || "0")) { this.default_focus_field = field; } @@ -957,7 +1009,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return this.fields_view.fields[field_name]; }, is_create_mode: function() { - return !this.datarecord.id; + return this.get("actual_mode") === "create"; }, open_translate_dialog: function(field) { return this._super(field); @@ -1641,10 +1693,13 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ this._super(view, node); this.force_disabled = false; this.string = (this.node.attrs.string || '').replace(/_/g, ''); - if (this.node.attrs.default_focus == '1') { + if (JSON.parse(this.node.attrs.default_focus || "0")) { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; } + if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { + this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; + } this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { @@ -2818,10 +2873,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc if (follow) link += "<br />"; } - this.$element.find('a') + var $link = this.$element.find('.oe_form_uri') .unbind('click') - .html(link) - .click(function () { + .html(link); + if (! this.get_definition_options().no_open) + $link.click(function () { self.do_action({ type: 'ir.actions.act_window', res_model: self.field.relation, @@ -2979,6 +3035,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ } if(view.view_type === "list") { _.extend(view.options, { + addable: null, selectable: self.multi_selection, sortable: false, import_enabled: false, @@ -2986,7 +3043,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }); if (self.get("effective_readonly")) { _.extend(view.options, { - addable: null, deletable: null, reorderable: false, }); @@ -3027,8 +3083,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(); @@ -3301,7 +3360,7 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ .value(); }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -3408,6 +3467,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super.apply(this, arguments); } }); +instance.web.form.One2ManyList = instance.web.ListView.List.extend({ + pad_table_to: function (count) { + this._super(count > 0 ? count - 1 : 0); + + // magical invocation of wtf does that do + if (this.view.o2m.get('effective_readonly')) { + return; + } + + var self = this; + var columns = _(this.columns).filter(function (column) { + return column.invisible !== '1'; + }).length; + if (this.options.selectable) { columns++; } + if (this.options.deletable) { columns++; } + var $cell = $('<td>', { + colspan: columns, + 'class': 'oe_form_field_one2many_list_row_add' + }).text(_t("Add a row")) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + self.view.do_add_record(); + }); + this.$current.append( + $('<tr>').append($cell)) + } +}); instance.web.form.One2ManyFormView = instance.web.FormView.extend({ form_template: 'One2Many.formview', @@ -3535,12 +3622,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in render_value: function() { var self = this; var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + var values = self.get("value") var handle_names = function(data) { var indexed = {}; _.each(data, function(el) { indexed[el[0]] = el; }); - data = _.map(self.get("value"), function(el) { return indexed[el]; }); + data = _.map(values, function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); $("textarea", self.$element).css("padding-left", "3px"); @@ -3549,8 +3637,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self.$element.html(QWeb.render("FieldMany2ManyTag", {elements: data})); } }; - if (! self.get('values') || self.get('values').length > 0) { - this._display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names); + if (! values || values.length > 0) { + this._display_orderer.add(dataset.name_get(values)).then(handle_names); } else { handle_names([]); } @@ -4108,10 +4196,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(); @@ -4343,7 +4433,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({ @@ -4353,7 +4443,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 f68f1354099..9c7bf88db1a 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, }, @@ -360,6 +357,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi { label: _t('Delete'), callback: this.do_delete_selected }, ]); this.sidebar.add_toolbar(this.fields_view.toolbar); + this.sidebar.$element.hide(); } }, /** @@ -378,6 +376,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi var total = dataset.size(); var limit = this.limit() || total; + this.$pager.toggle(total !== 0); this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit)); var spager = '-'; if (total) { @@ -496,9 +495,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, do_show: function () { this._super(); - if (this.sidebar) { - this.sidebar.$element.show(); - } if (this.$buttons) { this.$buttons.show(); } @@ -937,7 +933,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. '[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, attribute, value, old_value) { @@ -972,8 +967,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. '[data-id=' + previous_record.get('id') + ']'); $new_row.insertAfter($previous_sibling); } - - self.refresh_zebra(index, 1); } }; _(this.record_callbacks).each(function (callback, event) { @@ -1113,7 +1106,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.$current .children('tr:not([data-id])').remove().end() .append(new Array(count - this.records.length + 1).join(row)); - this.refresh_zebra(this.records.length); }, /** * Gets the ids of all currently selected records, if any @@ -1187,25 +1179,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. render_cell: function () { return self.render_cell.apply(self, arguments); } }); - }, - /** - * Fixes fixes the even/odd classes - * - * @param {Number} [from_index] index from which to resequence - * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table - */ - refresh_zebra: function (from_index, offset) { - offset = offset || 0; - from_index = from_index || 0; - var dom_offset = offset + from_index; - var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null; - this.$current.children(sel).each(function (i, e) { - var index = from_index + i; - // reset record-index accelerators on rows and even/odd - var even = index%2 === 0; - $(e).toggleClass('even', even) - .toggleClass('odd', !even); - }); } }); instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{ @@ -1538,8 +1511,6 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }(dataset, record.get('id'), seq)); record.set('sequence', seq); } - - list.refresh_zebra(); } }); }, diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 56df125c98b..ca889f68d8d 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -12,6 +12,8 @@ openerp.web.list_editable = function (instance) { 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 @@ -37,6 +39,11 @@ openerp.web.list_editable = function (instance) { } }); + 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'); }); @@ -62,26 +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) { - // TODO: fix handling of editability status to be simpler & clearer & more coherent - // 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); }, /** @@ -89,7 +86,7 @@ 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.start_edition(); @@ -103,9 +100,8 @@ openerp.web.list_editable = function (instance) { 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); var result = this._super(data, grouped); - if (this.options.editable) { + if (this.editable()) { // FIXME: any hook available to ensure this is only done once? this.$buttons .off('click', '.oe_list_save') @@ -202,12 +198,20 @@ openerp.web.list_editable = function (instance) { 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); + } }); }); }, @@ -377,7 +381,7 @@ openerp.web.list_editable = function (instance) { return this.reload_record(record); }, prepends_on_create: function () { - return this.options.editable === 'top'; + return this.editable() === 'top'; }, setup_events: function () { var self = this; @@ -399,18 +403,20 @@ openerp.web.list_editable = function (instance) { * * @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 {*} */ - _next: function (next_record) { + _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(); } - return self.start_edition( - self.records[next_record]( - saveInfo.record, {wraparound: true})); + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); }); }, keyup_ENTER: function () { @@ -419,46 +425,142 @@ openerp.web.list_editable = function (instance) { keyup_ESCAPE: function () { return this.cancel_edition(); }, + /** + * Gets the selection range (start, end) for the provided element, + * returns ``null`` if it can't get a range. + * + * @private + */ _text_selection_range: function (el) { - if (el.selectionStart !== undefined) { + var selectionStart; + try { + selectionStart = el.selectionStart; + } catch (e) { + // radio or checkbox throw on selectionStart access + return null; + } + if (selectionStart !== undefined) { return { - start: el.selectionStart, + start: selectionStart, end: el.selectionEnd }; - } else if(document.body.createTextRange) { + } else if (document.body.createTextRange) { throw new Error("Implement text range handling for MSIE"); var sel = document.body.createTextRange(); if (sel.parentElement() === el) { } } + // Element without selection ranges (select, div/@contenteditable) + return null; }, _text_cursor: function (el) { var selection = this._text_selection_range(el); - if (selection.start !== selection.end) { + if (!selection) { return null; } - return selection.start; + if (selection.start !== selection.end) { + return {position: null, collapsed: false}; + } + return {position: selection.start, collapsed: true}; }, - keydown_UP: function (e) { + /** + * Checks if the cursor is at the start of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private + */ + _at_start: function (cursor, el) { + return cursor.collapsed && (cursor.position === 0); + }, + /** + * Checks if the cursor is at the end of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private + */ + _at_end: function (cursor, el) { + return cursor.collapsed && (cursor.position === el.value.length); + }, + /** + * @param DOMEvent event + * @param {String} record_direction direction to move into to get the next record (pred | succ) + * @param {Function} is_valid_move whether the edition should be moved to the next record + * @private + */ + _key_move_record: function (event, record_direction, is_valid_move) { if (!this.editor.is_editing('edit')) { return $.when(); } - // FIXME: assumes editable widgets are input-type elements - var index = this._text_cursor(e.target); - // If selecting or not at the start of the input - if (index === null || index !== 0) { return $.when(); } + var cursor = this._text_cursor(event.target); + // if text-based input (has a cursor) + // and selecting (not collapsed) or not at a field boundary + // don't move to the next record + if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); } - e.preventDefault(); - return this._next('pred'); + 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) { + var self = this; + return this._key_move_record(e, 'pred', function (el, cursor) { + return self._at_start(cursor, el); + }); }, keydown_DOWN: function (e) { - if (!this.editor.is_editing('edit')) { return $.when(); } - // FIXME: assumes editable widgets are input-type elements - var index = this._text_cursor(e.target); - // If selecting or not at the end of the input - if (index === null || index !== e.target.value.length) { return $.when(); } + var self = this; + return this._key_move_record(e, 'succ', function (el, cursor) { + return self._at_end(cursor, el); + }); + }, - e.preventDefault(); - return this._next(); + 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 cursor = this._text_cursor(e.target); + if (cursor && !this._at_start(cursor, e.target)) { 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 cursor = this._text_cursor(e.target); + if (cursor && !this._at_end(cursor, e.target)) { 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; @@ -642,7 +744,7 @@ openerp.web.list_editable = function (instance) { instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ row_clicked: function (event) { - if (!this.options.editable) { + if (!this.view.editable()) { return this._super.apply(this, arguments); } var record_id = $(event.currentTarget).data('id'); diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index ccf34576a1a..10f44a54f11 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -120,7 +120,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ if (this.getParent() && this.getParent().do_push_state) { if (this.inner_action) { state['title'] = this.inner_action.name; - state['model'] = this.inner_action.res_model; + if(this.inner_action.type == 'ir.actions.act_window') { + state['model'] = this.inner_action.res_model; + } if (this.inner_action.id) { state['action_id'] = this.inner_action.id; } @@ -178,7 +180,10 @@ instance.web.ActionManager = instance.web.Widget.extend({ }); }, do_action: function(action, on_close) { - if (_.isNumber(action) || _.isString(action)) { + if (_.isString(action) && instance.web.client_actions.contains(action)) { + var action_client = { type: "ir.actions.client", tag: action }; + return this.do_action(action_client); + } else if (_.isNumber(action) || _.isString(action)) { var self = this; return self.rpc("/web/action/load", { action_id: action }, function(result) { self.do_action(result.result, on_close); @@ -209,21 +214,30 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog_stop(); this.clear_breadcrumbs(); }, - ir_actions_act_window: function (action, on_close) { - var self = this; - if (_(['base.module.upgrade', 'base.setup.installer']) - .contains(action.res_model)) { - var old_close = on_close; - on_close = function () { - instance.webclient.do_reload().then(old_close); - }; + + do_ir_actions_common: function(action, on_close) { + var self = this, klass, widget, add_breadcrumb; + if (action.type === 'ir.actions.client') { + var ClientWidget = instance.web.client_actions.get_object(action.tag); + widget = new ClientWidget(this, action.params); + klass = 'oe_act_client'; + add_breadcrumb = function() { + self.push_breadcrumb({ + widget: widget, + title: action.name + }); + } + } else { + widget = new instance.web.ViewManagerAction(this, action); + klass = 'oe_act_window'; + add_breadcrumb = widget.add_breadcrumb } if (action.target === 'new') { if (this.dialog === null) { // These buttons will be overwrited by <footer> if any this.dialog = new instance.web.Dialog(this, { buttons: { "Close": function() { $(this).dialog("close"); }}, - dialogClass: 'oe_act_window' + dialogClass: klass }); if(on_close) this.dialog.on_close.add(on_close); @@ -231,7 +245,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog_widget.destroy(); } this.dialog.dialog_title = action.name; - this.dialog_widget = new instance.web.ViewManagerAction(this, action); + this.dialog_widget = widget; this.dialog_widget.appendTo(this.dialog.$element); this.dialog.open(); } else { @@ -242,11 +256,34 @@ instance.web.ActionManager = instance.web.Widget.extend({ }); } this.inner_action = action; - var inner_widget = this.inner_widget = new instance.web.ViewManagerAction(this, action); - inner_widget.add_breadcrumb(); + this.inner_widget = widget; + add_breadcrumb(); this.inner_widget.appendTo(this.$element); } }, + + ir_actions_act_window: function (action, on_close) { + var self = this; + if (_(['base.module.upgrade', 'base.setup.installer']) + .contains(action.res_model)) { + var old_close = on_close; + on_close = function () { + instance.webclient.do_reload().then(old_close); + }; + } + if (action.target !== 'new') { + if(action.menu_id) { + this.dialog_stop(); + return this.getParent().do_action(action, function () { + instance.webclient.menu.open_menu(action.menu_id); + }); + } + } + return this.do_ir_actions_common(action, on_close); + }, + ir_actions_client: function (action, on_close) { + return this.do_ir_actions_common(action, on_close); + }, ir_actions_act_window_close: function (action, on_closed) { if (!this.dialog && on_closed) { on_closed(); @@ -262,19 +299,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.do_action(action, on_closed) }); }, - ir_actions_client: function (action) { - this.dialog_stop(); - var ClientWidget = instance.web.client_actions.get_object(action.tag); - this.inner_widget = new ClientWidget(this, action.params); - this.push_breadcrumb({ - widget: this.inner_widget, - title: action.name - }); - this.inner_widget.appendTo(this.$element); - }, 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 +311,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 +868,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 +1007,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) { |
