diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 9e8e258dedc..4051e607e21 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -20,6 +20,7 @@ "static/lib/jquery.form/jquery.form.js", "static/lib/jquery.validate/jquery.validate.js", "static/lib/jquery.ba-bbq/jquery.ba-bbq.js", + "static/lib/spinjs/spin.js", "static/lib/jquery.blockUI/jquery.blockUI.js", "static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js", "static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js", diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 2e26de30d5f..2be11ff020d 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1,4 +1,4 @@ -@charset "UTF-8"; +@charset "utf-8"; @font-face { font-family: "mnmliconsRegular"; src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot"); @@ -1408,20 +1408,31 @@ filter: alpha(opacity=50); opacity: 0.5; } +.openerp .oe_searchview .oe_searchview_search { + font-size: 1px; + letter-spacing: -1px; + color: transparent; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + position: absolute; + left: 3px; + top: 1px; + padding: 0; + border: none; + background: transparent; +} +.openerp .oe_searchview .oe_searchview_search:before { + font: 21px "mnmliconsRegular"; + content: "r"; + color: #a3a3a3; +} .openerp .oe_searchview .oe_searchview_facets { min-height: 22px; -} -.openerp .oe_searchview .oe_searchview_facets:before { - color: #cccccc; - font-family: "mnmliconsRegular"; - content: "r"; - font-size: 130%; - display: inline; - position: relative; - left: 6px; - top: 2px; - color: #a3a3a3; - padding-right: 4px; + margin-left: 15px; } .openerp .oe_searchview .oe_searchview_facets * { vertical-align: top; @@ -2180,7 +2191,7 @@ height: auto; line-height: 16px; } -.openerp .oe_form_field_one2many .oe_list_buttons.oe_editing .oe_list_save { +.openerp .oe_form_field_one2many .oe_list_buttons.oe_editing .oe_list_save, .openerp .oe_form_field_many2many .oe_list_buttons.oe_editing .oe_list_save { visibility: hidden; } .openerp .oe_form .oe_form_field_many2many > .oe_list .oe_list_pager_single_page { @@ -2266,9 +2277,6 @@ padding: 3px 6px; white-space: pre-line; } -.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell progress { - width: 100%; -} .openerp .oe_list_content > tbody > tr > td > button, .openerp .oe_list_content > tbody > tr > th > button { border: none; background: transparent; @@ -2441,19 +2449,6 @@ .kitten-mode-activated > * { opacity: 0.7; } -.kitten-mode-activated .oe_footer a { - background-image: url(http://www.risacher.com/la-rache/zfiles/la-rache.png); - font-size: 1px; - letter-spacing: -1px; - color: transparent; - display: inline-block; - height: 15px; - width: 80px; - vertical-align: top; -} -.kitten-mode-activated .oe_footer a span { - display: none; -} div.ui-widget-overlay { background: black; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index c4314923867..aef356d3ab6 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1101,19 +1101,20 @@ $sheet-max-width: 860px border-right: 5px solid transparent @include opacity() + .oe_searchview_search + @include text-to-icon("r", #a3a3a3) + @include box-shadow(none) + @include radius(0) + position: absolute + left: 3px + top: 1px + padding: 0 + border: none + background: transparent + .oe_searchview_facets min-height: 22px - &:before - color: #ccc - font-family: "mnmliconsRegular" - content: "r" - font-size: 130% - display: inline - position: relative - left: 6px - top: 2px - color: #a3a3a3 - padding-right: 4px + margin-left: 15px * vertical-align: top display: inline-block diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif deleted file mode 100644 index 5d2057ae0e7..00000000000 Binary files a/addons/web/static/src/img/throbber2.gif and /dev/null differ diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 71010ef6d74..7212108478d 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 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 = ''; + $.blockUI.defaults.message = '
'; + $.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..8ffd672c143 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; } diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 677b7cf7c4e..835527b3135 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3027,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(); @@ -3301,7 +3304,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; @@ -4108,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(); @@ -4343,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({ @@ -4353,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 f68f1354099..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, }, diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 56df125c98b..c7654479864 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 () { @@ -440,25 +446,80 @@ openerp.web.list_editable = function (instance) { } return selection.start; }, - keydown_UP: function (e) { + /** + * @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); + var index = this._text_cursor(event.target); // If selecting or not at the start of the input - if (index === null || index !== 0) { return $.when(); } + if (!is_valid_move(event.target, index)) { 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) { + return this._key_move_record(e, 'pred', function (el, index) { + return index === 0; + }); }, 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(); } + return this._key_move_record(e, 'succ', function (el, index) { + return index === el.value.length; + }); + }, - 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 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; @@ -642,7 +703,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..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) { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index ed63b5fbfbe..e4aa6797ba6 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1220,6 +1220,8 @@
+
`. The editable list view module adds a number of methods to the list view, on top of implementing the :js:class:`EditorDelegate` protocol: @@ -219,6 +222,14 @@ view provides a number of dedicated events to its lifecycle. abort its current behavior as soon as possible, and rollback any state modification. + Generally speaking, an event should only be cancelled (by + setting the ``cancel`` flag to ``true``), uncancelling an + event is undefined as event handlers are executed on a + first-come-first-serve basis and later handlers may + re-cancel an uncancelled event. + +.. _listview-edit-before: + ``edit:before`` *cancellable* Invoked before the list view starts editing a record.