diff options
| author | Antony Lesuisse <al@openerp.com> | 2012-07-25 10:26:46 +0200 |
|---|---|---|
| committer | Antony Lesuisse <al@openerp.com> | 2012-07-25 10:26:46 +0200 |
| commit | 3235ed8a1896fbf24e61982e4c06b7d7f0f1815b (patch) | |
| tree | c563cc1ae571e10d95804cb99ff0e7e51a452986 /addons/web/static/src | |
| parent | 974d4f4f106762dafb82b6d6f7a7a43fd6cdacdb (diff) | |
| parent | a4f1ce69ab01818fce1a23fc3744865bcc4fd85c (diff) | |
[MERGE] trunk
bzr revid: al@openerp.com-20120725082646-ff6ljt0pquvm0dup
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/css/base.css | 122 | ||||
| -rw-r--r-- | addons/web/static/src/css/base.sass | 106 | ||||
| -rw-r--r-- | addons/web/static/src/img/pattern.png | bin | 98 -> 0 bytes | |||
| -rw-r--r-- | addons/web/static/src/img/throbber.gif | bin | 673 -> 7625 bytes | |||
| -rw-r--r-- | addons/web/static/src/img/throbber2.gif | bin | 7195 -> 0 bytes | |||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 35 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 19 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 310 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 158 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 929 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 16 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 65 |
14 files changed, 1182 insertions, 592 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 0a3d24d41f4..30c0b5ea0b3 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"); @@ -267,6 +267,45 @@ color: black; text-decoration: none; } +.openerp.ui-dialog .oe_about { + background-color: white; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=); + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} +.openerp.ui-dialog .oe_about a { + color: #8a89ba; +} +.openerp.ui-dialog .oe_about a:hover { + text-decoration: underline; +} +.openerp.ui-dialog .oe_about .oe_logo { + margin-left: -6px; +} +.openerp.ui-dialog .oe_about .oe_bottom { + position: absolute; + top: 50%; + left: 0; + right: 0; + bottom: 0; + text-shadow: 0 1px 1px #999999; + background-color: #b41616; + background-image: -webkit-gradient(linear, left top, left bottom, from(#b41616), to(#600606)); + background-image: -webkit-linear-gradient(top, #b41616, #600606); + background-image: -moz-linear-gradient(top, #b41616, #600606); + background-image: -ms-linear-gradient(top, #b41616, #600606); + background-image: -o-linear-gradient(top, #b41616, #600606); + background-image: linear-gradient(to bottom, #b41616, #600606); + color: #eeeeee; + padding: 0 16px; + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} +.openerp.ui-dialog .oe_about .oe_bottom a { + color: #eeeeee; +} .openerp.ui-dialog.oe_act_window .ui-dialog-content { padding: 0px; } @@ -584,7 +623,7 @@ z-index: 1; border: 1px solid #afafb6; background: white; - padding: 6px 0; + padding: 4px 0; min-width: 140px; text-align: left; -moz-border-radius: 3px; @@ -599,6 +638,7 @@ float: none; display: block; position: relative; + padding: 2px 8px; } .openerp .oe_dropdown_menu > li:hover { background-color: #f0f0fa; @@ -615,7 +655,6 @@ .openerp .oe_dropdown_menu > li > a { white-space: nowrap; display: block; - padding: 4px 15px; color: #4c4c4c; text-decoration: none; } @@ -711,7 +750,7 @@ z-index: 1050; } .openerp .oe_login { - background: url("/web/static/src/img/pattern.png") repeat; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=); text-align: center; font-size: 14px; height: 100%; @@ -945,11 +984,12 @@ } .openerp .oe_topbar .oe_dropdown_menu li { float: none; + padding: 3px 12px; } .openerp .oe_topbar .oe_dropdown_menu li a { color: #eeeeee; } -.openerp .oe_topbar .oe_dropdown_menu li a:hover { +.openerp .oe_topbar .oe_dropdown_menu li:hover { background-color: #292929; background-image: -webkit-gradient(linear, left top, left bottom, from(#292929), to(#191919)); background-image: -webkit-linear-gradient(top, #292929, #191919); @@ -1408,20 +1448,31 @@ filter: alpha(opacity=50); opacity: 0.5; } -.openerp .oe_searchview .oe_searchview_facets { - min-height: 22px; +.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_facets:before { - color: #cccccc; - font-family: "mnmliconsRegular"; +.openerp .oe_searchview .oe_searchview_search:before { + font: 21px "mnmliconsRegular"; content: "r"; - font-size: 130%; - display: inline; - position: relative; - left: 6px; - top: 2px; color: #a3a3a3; - padding-right: 4px; +} +.openerp .oe_searchview .oe_searchview_facets { + min-height: 22px; + margin-left: 15px; } .openerp .oe_searchview .oe_searchview_facets * { vertical-align: top; @@ -1437,7 +1488,7 @@ outline: none; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_input { - padding: 0 3px; + padding: 0 0 0 6px; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet { position: relative; @@ -1600,14 +1651,14 @@ .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section li:hover { background-color: #f0f0fa; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form { +.openerp .oe_searchview .oe_searchview_drawer form { margin-left: 12px; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form p { +.openerp .oe_searchview .oe_searchview_drawer form p { margin: 4px 0; line-height: 18px; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form button { +.openerp .oe_searchview .oe_searchview_drawer form button { margin: 0 0 8px 0; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom { @@ -2180,9 +2231,33 @@ height: auto; line-height: 16px; } +.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 { display: none; } +.openerp .oe_list_buttons .oe_list_save, .openerp .oe_list_buttons .oe_list_discard { + display: none; +} +.openerp .oe_list_buttons.oe_editing .oe_list_add, .openerp .oe_list_buttons.oe_editing .oe_list_button_import { + display: none; +} +.openerp .oe_list_buttons.oe_editing .oe_list_save { + display: inline-block; +} +.openerp .oe_list_buttons.oe_editing .oe_list_discard { + display: inline; +} +.openerp .oe_list { + position: relative; +} +.openerp .oe_list .oe_form .oe_form_field { + width: auto; + position: absolute; + margin: 0 !important; + padding: 0; +} .openerp .oe_list_content { width: 100%; } @@ -2240,9 +2315,7 @@ } .openerp .oe_list_content > tbody > tr > td.oe_list_field_cell { padding: 3px 6px; -} -.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell progress { - width: 100%; + white-space: pre-line; } .openerp .oe_list_content > tbody > tr > td > button, .openerp .oe_list_content > tbody > tr > th > button { border: none; @@ -2290,9 +2363,6 @@ .openerp .oe_list_content .numeric input { text-align: right; } -.openerp .oe_list_content .oe_list_edit_row_save:before { - content: "S"; -} .openerp .oe_trad_field.touched { border: 1px solid green !important; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index fb67945a02d..7fbe5c1c336 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -92,9 +92,8 @@ $sheet-max-width: 860px letter-spacing: -1px color: transparent &:before - font-family: "mnmliconsRegular" + font: 21px "mnmliconsRegular" content: $icon-name - font-size: 20px color: $color // }}} @@ -256,6 +255,29 @@ $sheet-max-width: 860px &:hover color: black text-decoration: none + .oe_about + background-color: white + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=) + @include radius(0 0 2px 2px) + a + color: #8A89BA + &:hover + text-decoration: underline + .oe_logo + margin-left: -6px + .oe_bottom + position: absolute + top: 50% + left: 0 + right: 0 + bottom: 0 + text-shadow: 0 1px 1px #999999 + @include vertical-gradient(#b41616, #600606) + color: #eee + padding: 0 16px + @include radius(0 0 2px 2px) + a + color: #eee &.ui-dialog.oe_act_window .ui-dialog-content @@ -470,7 +492,7 @@ $sheet-max-width: 860px z-index: 1 border: 1px solid #afafb6 background: white - padding: 6px 0 + padding: 4px 0 min-width: 140px text-align: left @include radius(3px) @@ -483,10 +505,10 @@ $sheet-max-width: 860px float: none display: block position: relative + padding: 2px 8px > a white-space: nowrap display: block - padding: 4px 15px color: #4c4c4c text-decoration: none &:hover @@ -567,7 +589,7 @@ $sheet-max-width: 860px // }}} // Login {{{ .oe_login - background: url("/web/static/src/img/pattern.png") repeat + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=) text-align: center font-size: 14px height: 100% @@ -735,11 +757,12 @@ $sheet-max-width: 860px @include background-clip() li float: none + padding: 3px 12px a color: #eee - &:hover - @include vertical-gradient(#292929, #191919) - @include box-shadow(none) + &:hover + @include vertical-gradient(#292929, #191919) + @include box-shadow(none) // }}} // Webclient.leftbar {{{ @@ -1102,19 +1125,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 @@ -1126,7 +1150,7 @@ $sheet-max-width: 860px &:focus outline: none .oe_searchview_input - padding: 0 3px + padding: 0 0 0 6px .oe_searchview_facet position: relative cursor: pointer @@ -1248,13 +1272,13 @@ $sheet-max-width: 860px // after oe_selected so background color is not overridden &:hover background-color: $hover-background - form - margin-left: 12px - p - margin: 4px 0 - line-height: 18px - button - margin: 0 0 8px 0 + form + margin-left: 12px + p + margin: 4px 0 + line-height: 18px + button + margin: 0 0 8px 0 .oe_searchview_custom padding: 0 8px 8px 8px form @@ -1721,6 +1745,9 @@ $sheet-max-width: 860px li height: auto line-height: 16px + .oe_list_buttons.oe_editing .oe_list_save + // keep "save row" button hidden in o2m + visibility: hidden // }}} // FormView.many2many {{{ .oe_form .oe_form_field_many2many > .oe_list @@ -1728,6 +1755,25 @@ $sheet-max-width: 860px display: none // }}} // ListView {{{ + .oe_list_buttons + .oe_list_save, .oe_list_discard + display: none + &.oe_editing + .oe_list_add, .oe_list_button_import + display: none + .oe_list_save + display: inline-block + .oe_list_discard + display: inline + + .oe_list + position: relative + .oe_form .oe_form_field + width: auto + position: absolute + margin: 0 !important // dammit + padding: 0 + .oe_list_content width: 100% td:first-child, th:first-child @@ -1771,8 +1817,7 @@ $sheet-max-width: 860px border-top: 1px solid #ddd > td.oe_list_field_cell padding: 3px 6px - progress - width: 100% + white-space: pre-line > td, > th > button border: none @@ -1800,8 +1845,6 @@ $sheet-max-width: 860px width: 82px input text-align: right - .oe_list_edit_row_save:before - content: "S" // }}} // Translation {{{ .oe_trad_field.touched @@ -1910,6 +1953,7 @@ $sheet-max-width: 860px background-attachment: fixed >* opacity: 0.70 + // }}} div.ui-widget-overlay diff --git a/addons/web/static/src/img/pattern.png b/addons/web/static/src/img/pattern.png Binary files differdeleted file mode 100644 index 7e3a2b37834..00000000000 --- a/addons/web/static/src/img/pattern.png +++ /dev/null diff --git a/addons/web/static/src/img/throbber.gif b/addons/web/static/src/img/throbber.gif Binary files differindex ded8d36d486..7ab8346ce09 100644 --- a/addons/web/static/src/img/throbber.gif +++ b/addons/web/static/src/img/throbber.gif diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif Binary files differdeleted file mode 100644 index 5d2057ae0e7..00000000000 --- a/addons/web/static/src/img/throbber2.gif +++ /dev/null diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 40cff06e66a..b5c69f5c82f 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -135,8 +135,12 @@ instance.web.Dialog = instance.web.Widget.extend({ this.$element.dialog('close'); }, on_close: function() { + if (this.__tmp_dialog_destroying) + return; if (this.dialog_options.destroy_on_close) { + this.__tmp_dialog_closing = true; this.destroy(); + this.__tmp_dialog_closing = undefined; } }, on_resized: function() { @@ -145,6 +149,11 @@ instance.web.Dialog = instance.web.Widget.extend({ _.each(this.getChildren(), function(el) { el.destroy(); }); + if (! this.__tmp_dialog_closing) { + this.__tmp_dialog_destroying = true; + this.close(); + this.__tmp_dialog_destroying = undefined; + } if (! this.isDestroyed()) { this.$element.dialog('destroy'); } @@ -248,13 +257,17 @@ instance.web.Loading = instance.web.Widget.extend({ // Block UI after 3s this.long_running_timer = setTimeout(function () { self.blocked_ui = true; - $.blockUI(); + instance.web.blockUI(); }, 3000); } this.count += increment; if (this.count > 0) { - this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + if (instance.connection.debug) { + this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + } else { + this.$element.text(_t("Loading")); + } this.$element.show(); this.getParent().$element.addClass('oe_wait'); } else { @@ -263,7 +276,7 @@ instance.web.Loading = instance.web.Widget.extend({ // Don't unblock if blocked by somebody else if (self.blocked_ui) { this.blocked_ui = false; - $.unblockUI(); + instance.web.unblockUI(); } this.$element.fadeOut(); this.getParent().$element.removeClass('oe_wait'); @@ -274,7 +287,7 @@ instance.web.Loading = instance.web.Widget.extend({ instance.web.DatabaseManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); - this.unblockUIFunction = $.unblockUI; + this.unblockUIFunction = instance.web.unblockUI; $.validator.addMethod('matches', function (s, _, re) { return new RegExp(re).test(s); }, _t("Invalid database name")); @@ -341,16 +354,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ * from unblocking the UI */ blockUI: function () { - $.blockUI(); - $.unblockUI = function () {}; + instance.web.blockUI(); + instance.web.unblockUI = function () {}; }, /** * Reinstates $.unblockUI so third parties can play with blockUI, and * unblocks the UI */ unblockUI: function () { - $.unblockUI = this.unblockUIFunction; - $.unblockUI(); + instance.web.unblockUI = this.unblockUIFunction; + instance.web.unblockUI(); }, /** * Displays an error dialog resulting from the various RPC communications @@ -851,7 +864,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ window.location.href, 'debug'); }); instance.web.dialog($help, {autoOpen: true, - modal: true, width: 960, title: _t("About")}); + modal: true, width: 580, height: 290, resizable: false, title: _t("About")}); }); }, }); @@ -890,7 +903,7 @@ instance.web.Client = instance.web.Widget.extend({ var doc_width = $(document).width(); var offset = $menu.offset(); var menu_width = $menu.width(); - var x = doc_width - offset.left - menu_width - 15; + var x = doc_width - offset.left - menu_width - 2; if (x < 0) { $menu.offset({ left: offset.left + x }).width(menu_width); } @@ -957,10 +970,12 @@ instance.web.WebClient = instance.web.Client.extend({ }, show_login: function() { var self = this; + self.$('.oe_topbar').hide(); self.login.appendTo(self.$element); }, show_application: function() { var self = this; + self.$('.oe_topbar').show(); self.login.$element.hide(); self.menu = new instance.web.Menu(self); self.menu.replace(this.$element.find('.oe_menu_placeholder')); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 7d101874c07..c9015220ba7 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -550,7 +550,15 @@ $.async_when = function() { /** Setup blockui */ if ($.blockUI) { $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; + $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber.gif">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; +} +instance.web.blockUI = function() { + $.blockUI.apply($, arguments); +} +instance.web.unblockUI = function() { + return $.unblockUI.apply($, arguments); } /** Setup default session */ diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index ebe82ddb4b4..f8e669e1b4a 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -376,7 +376,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ exported_fields.unshift({name: 'id', label: 'External ID'}); var export_format = this.$element.find("#export_format").val(); - $.blockUI(); + instance.web.blockUI(); this.session.get_file({ url: '/web/export/' + export_format, data: {data: JSON.stringify({ @@ -387,7 +387,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ import_compat: Boolean( this.$element.find("#import_compat").val()) })}, - complete: $.unblockUI + complete: instance.web.unblockUI }); }, close: function() { diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 165e47d1f20..7e070e406f8 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -330,6 +330,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea }); } + // Launch a search on clicking the oe_searchview_search button + this.$element.on('click', 'button.oe_searchview_search', function (e) { + e.stopImmediatePropagation(); + self.do_search(); + }); + this.$element.on('keydown', '.oe_searchview_input, .oe_searchview_facet', function (e) { switch(e.which) { @@ -1475,7 +1481,17 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ facet_for: function (value) { var self = this; if (value instanceof Array) { - return $.when(facet_from(this, value)); + if (value.length === 2 && _.isString(value[1])) { + return $.when(facet_from(this, value)); + } + if (value.length > 1) { + // more than one search_default m2o id? Should we OR them? + throw new Error( + _("M2O search fields do not currently handle multiple default values")); + } + // there are many cases of {search_default_$m2ofield: [id]}, need + // to handle this as if it were a single value. + value = value[0]; } return this.model.call('name_get', [value], {}).pipe(function (names) { if (_(names).isEmpty()) { return null; } @@ -1669,6 +1685,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({ return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data")); }, load_data:function(){ + if (!instance.webclient) { return $.Deferred().reject(); } var self = this,dashboard_menu = instance.webclient.menu.data.data.children; var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']); var map_data = function(result){ diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index d6ed85fedad..835527b3135 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -239,6 +239,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } } }, + /** + * + * @param {Object} [options] + * @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode. + * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @return {$.Deferred} + */ do_show: function (options) { var self = this; options = options || {}; @@ -253,23 +260,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } this.$element.show().css('visibility', 'hidden'); this.$element.add(this.$buttons).removeClass('oe_form_dirty'); - return this.has_been_loaded.pipe(function() { - var result; - if (self.dataset.index === null) { - // null index means we should start a new record - result = self.on_button_new(); - } else { - result = self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } - }).pipe(self.on_record_loaded); - } - result.pipe(function() { - if (options.editable) { - self.set({mode: "edit"}); + + var shown = this.has_been_loaded; + if (options.reload !== false) { + shown = shown.pipe(function() { + if (self.dataset.index === null) { + // null index means we should start a new record + return self.on_button_new(); } - self.$element.css('visibility', 'visible'); + return self.dataset.read_index(_.keys(self.fields_view.fields), { + context: { 'bin_size': true } + }).pipe(self.on_record_loaded); }); - return result; + } + return shown.pipe(function() { + if (options.editable) { + self.set({mode: "edit"}); + } + self.$element.css('visibility', 'visible'); }); }, do_hide: function () { @@ -330,6 +338,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.$element.add(self.$buttons).removeClass('oe_form_dirty'); }); }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys) + .pipe(this.on_record_loaded); + } + return this.on_record_loaded({}); + }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, @@ -600,22 +622,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM on_button_new: function() { var self = this; this.set({mode: "edit"}); - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { + return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { - var keys = _.keys(self.fields_view.fields); - if (keys.length) { - self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { - def.resolve(); - }); - } else { - self.on_record_loaded({}).then(function() { - def.resolve(); - }); - } + return self.load_defaults(); } }); - return def.promise(); }, on_button_edit: function() { return this.set({mode: "edit"}); @@ -674,6 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM values = {}, first_invalid_field = null; for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; @@ -689,8 +701,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } if (form_invalid) { self.set({'display_invalid_fields': true}); - for (var f in self.fields) { - self.fields[f]._check_css_flags(); + for (var g in self.fields) { + if (!self.fields.hasOwnProperty(g)) { continue; } + self.fields[g]._check_css_flags(); } first_invalid_field.focus(); self.on_invalid(); @@ -722,14 +735,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM });}); }, on_invalid: function() { - var msg = "<ul>"; - _.each(this.fields, function(f) { - if (!f.is_valid()) { - msg += "<li>" + f.string + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); + var warnings = _(this.fields).chain() + .filter(function (f) { return !f.is_valid(); }) + .map(function (f) { + return _.str.sprintf('<li>%s</li>', + _.escape(f.string)); + }).value(); + warnings.unshift('<ul>'); + warnings.push('</ul>'); + this.do_warn("The following fields are invalid :", warnings.join('')); }, on_saved: function(r, success) { if (!r.result) { @@ -807,10 +821,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { - if (_.include(blacklist, key)) + if (_.include(blacklist, key)) { return; - var val = value_.get_value(); - values[key] = val; + } + values[key] = value_.get_value(); }); return values; }, @@ -945,6 +959,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM is_create_mode: function() { return !this.datarecord.id; }, + open_translate_dialog: function(field) { + return this._super(field); + }, }); /** @@ -988,7 +1005,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } }); } - selector = 'form[version!="7.0"] page,form[version!="7.0"]'; }, render_to: function($target) { var self = this; @@ -1122,7 +1138,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - $label = $('<label/>').attr({ + var $label = $('<label/>').attr({ 'for' : name, "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), "string": $field.attr('string'), @@ -1336,7 +1352,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $new_label; }, handle_common_properties: function($new_element, $node) { - var str_modifiers = $node.attr("modifiers") || "{}" + var str_modifiers = $node.attr("modifiers") || "{}"; var modifiers = JSON.parse(str_modifiers); var ic = null; if (modifiers.invisible !== undefined) @@ -1455,24 +1471,27 @@ instance.web.form.compute_domain = function(expr, fields) { */ instance.web.form.InvisibilityChangerMixin = { init: function(field_manager, invisible_domain) { - this._ic_field_manager = field_manager + var self = this; + this._ic_field_manager = field_manager; this._ic_invisible_modifier = invisible_domain; this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = this._ic_invisible_modifier === undefined ? false : - instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); - this.set({"invisible": result}); + var result = self._ic_invisible_modifier === undefined ? false : + instance.web.form.compute_domain( + self._ic_invisible_modifier, + self._ic_field_manager.fields); + self.set({"invisible": result}); }); this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); var check = function() { - if (this.get("invisible") || this.get('force_invisible')) { - this.set({"effective_invisible": true}); + if (self.get("invisible") || self.get('force_invisible')) { + self.set({"effective_invisible": true}); } else { - this.set({"effective_invisible": false}); + self.set({"effective_invisible": false}); } }; this.on('change:invisible', this, check); this.on('change:force_invisible', this, check); - _.bind(check, this)(); + check.call(this); }, start: function() { this.on("change:effective_invisible", this, this._check_visibility); @@ -1536,6 +1555,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi var compute_domain = instance.web.form.compute_domain; var to_set = {}; for (var a in this.modifiers) { + if (!this.modifiers.hasOwnProperty(a)) { continue; } if (!_.include(["invisible"], a)) { var val = compute_domain(this.modifiers[a], this.view.fields); to_set[a] = val; @@ -1682,11 +1702,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ on_confirmed: function() { var self = this; - var context = this.node.attrs.context; - if (context && context.__ref) { - context = new instance.web.CompoundContext(context); - context.set_eval_context(this._build_eval_context()); - } + var context = this.build_context(); return this.view.do_execute_action( _.extend({}, this.node.attrs, {context: context}), @@ -1739,18 +1755,18 @@ instance.web.form.FieldInterface = { /** * Get the current value of the widget. * - * Must always return a syntaxically correct value to be passed to the "write" method of the osv class in + * Must always return a syntactically correct value to be passed to the "write" method of the osv class in * the OpenERP server, although it is not assumed to respect the constraints applied to the field. - * For example if the field is marqued as "required", a call to get_value() can return false. + * For example if the field is marked as "required", a call to get_value() can return false. * * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to - * return a defaut value according to the type of field. + * return a default value according to the type of field. * * This method is always assumed to perform synchronously, it can not return a promise. * * If there was no user interaction to modify the value of the field, it is always assumed that * get_value() return the same semantic value than the one passed in the last call to set_value(), - * altough the syntax can be different. This can be the case for type of fields that have a different + * although the syntax can be different. This can be the case for type of fields that have a different * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). */ get_value: function() {}, @@ -1765,7 +1781,7 @@ instance.web.form.FieldInterface = { */ is_valid: function() {}, /** - * Returns true if the field holds a value which is syntaxically correct, ignoring + * Returns true if the field holds a value which is syntactically correct, ignoring * the potential semantic restrictions applied to the field. */ is_syntax_valid: function() {}, @@ -1795,6 +1811,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w * @param node */ init: function(field_manager, node) { + var self = this this._super(field_manager, node); this.field_manager = field_manager; this.name = this.node.attrs.name; @@ -1809,12 +1826,11 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w this.set({"readonly": this.modifiers['readonly'] === true}); this.set({"force_readonly": false}); var test_effective_readonly = function() { - this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); + self.set({"effective_readonly": self.get("readonly") || !!self.get("force_readonly")}); }; this.on("change:readonly", this, test_effective_readonly); this.on("change:force_readonly", this, test_effective_readonly); - _.bind(test_effective_readonly, this)(); - + test_effective_readonly.call(this); this.on("change:value", this, function() { if (! this._inhibit_on_change) this.trigger('changed_value'); @@ -1891,7 +1907,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w */ delay_focus: function($elem) { setTimeout(function() { - $elem.focus(); + $elem[0].focus(); }, 50); }, /** @@ -2234,6 +2250,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we } else { this.$textarea.attr('disabled', 'disabled'); } + this.$element.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); this.setupFocus(this.$textarea); }, set_value: function(value_) { @@ -2337,6 +2358,7 @@ instance.web.form.FieldTextHtml = instance.web.form.FieldText.extend({ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { + var self = this; this._super.apply(this, arguments); this.$checkbox = $("input", this.$element); this.setupFocus(this.$checkbox); @@ -2344,10 +2366,10 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ this.set({'value': this.$checkbox.is(':checked')}); }, this)); var check_readonly = function() { - this.$checkbox.prop('disabled', this.get("effective_readonly")); + self.$checkbox.prop('disabled', self.get("effective_readonly")); }; this.on("change:effective_readonly", this, check_readonly); - _.bind(check_readonly, this)(); + check_readonly.call(this); }, set_value: function(value_) { this._super.apply(this, arguments); @@ -2960,6 +2982,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ selectable: self.multi_selection, sortable: false, import_enabled: false, + deletable: true }); if (self.get("effective_readonly")) { _.extend(view.options, { @@ -3004,8 +3027,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - if (self.get("effective_readonly")) - controller.set_editable(false); + if (self.get("effective_readonly")) { + controller.on('edit:before', self, function (e) { + e.cancel = true; + }); + } } else if (view_type === "form") { if (self.get("effective_readonly")) { $(".oe_form_buttons", controller.$element).children().remove(); @@ -3189,6 +3215,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form: 'instance.web.form.One2ManyFormView', kanban: 'instance.web.form.One2ManyKanbanView', }); + this.__ignore_blur = false; }, switch_view: function(mode, unused) { if (mode !== 'form') { @@ -3238,19 +3265,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super(parent, dataset, view_id, _.extend(options || {}, { ListType: instance.web.form.One2ManyList })); + this.on('edit:before', this, this.proxy('_before_edit')); + this.on('save:before cancel:before', this, this.proxy('_before_unedit')); + + this.records + .bind('add', this.proxy("changed_records")) + .bind('edit', this.proxy("changed_records")) + .bind('remove', this.proxy("changed_records")); + }, + start: function () { + var ret = this._super(); + this.$element + .off('mousedown.handleButtons') + .on('mousedown.handleButtons', 'table button', this.proxy('_button_down')); + return ret; + }, + changed_records: function () { + this.o2m.trigger_on_change(); }, is_valid: function () { - var form; - // A list not being edited is always valid - if (!(form = this.first_edition_form())) { - return true; - } + var form = this.editor.form; + // If the form has not been modified, the view can only be valid // NB: is_dirty will also be set on defaults/onchanges/whatever? // oe_form_dirty seems to only be set on actual user actions if (!form.$element.is('.oe_form_dirty')) { return true; } + this.o2m._dirty_flag = true; // Otherwise validate internal form return _(form.fields).chain() @@ -3261,21 +3303,8 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ .all(_.identity) .value(); }, - first_edition_form: function () { - var get_form = function (group_or_list) { - if (group_or_list.edition) { - return group_or_list.edition_form; - } - return _(group_or_list.children).chain() - .map(get_form) - .compact() - .first() - .value(); - }; - return get_form(this.groups); - }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -3328,55 +3357,58 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ }); }, do_button_action: function (name, id, callback) { - var _super = _.bind(this._super, this); - - this.o2m.view.do_save().then(function () { - _super(name, id, callback); - }); - } -}); -instance.web.form.One2ManyList = instance.web.ListView.List.extend({ - KEY_RETURN: 13, - // blurring caused by hitting the [Return] key, should skip the - // autosave-on-blur and let the handler for [Return] do its thing - __return_blur: false, - render_row_as_form: function () { + if (!_.isNumber(id)) { + instance.webclient.notification.warn( + _t("Action Button"), + _t("The o2m record must be saved before an action can be used")); + return; + } + var parent_form = this.o2m.view; var self = this; - return this._super.apply(this, arguments).then(function () { - // Replace the "Save Row" button with "Cancel Edition" - self.edition_form.$element - .undelegate('button.oe_list_edit_row_save', 'click') - .delegate('button.oe_list_edit_row_save', 'click', function () { - self.cancel_pending_edition(); - }); - - // Overload execute_action on the edition form to perform a simple - // reload_record after the action is done, rather than fully - // reload the parent view (or something) - var _execute_action = self.edition_form.do_execute_action; - self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) { - return _execute_action.call(this, action, dataset, record_id, function () { - self.view.reload_record( - self.view.records.get(record_id)); - }); - }; - - self.edition_form.on('blurred', null, function () { - if (self.__return_blur) { - delete self.__return_blur; - return; - } - if (!self.edition_form.widget_is_stopped) { - self.view.ensure_saved(); - } - }); + this.ensure_saved().pipe(function () { + return parent_form.do_save(); + }).then(function () { + self.handle_button(name, id, callback); }); }, - on_row_keyup: function (e) { - if (e.which === this.KEY_RETURN) { - this.__return_blur = true; + + _before_edit: function () { + this.__ignore_blur = false; + this.editor.form.on('blurred', this, this._on_form_blur); + }, + _before_unedit: function () { + this.editor.form.off('blurred', this, this._on_form_blur); + }, + _button_down: function () { + // If a button is clicked (usually some sort of action button), it's + // the button's responsibility to ensure the editable list is in the + // correct state -> ignore form blurring + this.__ignore_blur = true; + }, + /** + * Handles blurring of the nested form (saves the currently edited row), + * unless the flag to ignore the event is set to ``true`` + * + * Makes the internal form go away + */ + _on_form_blur: function () { + if (this.__ignore_blur) { + this.__ignore_blur = false; + return; } - this._super(e); + // FIXME: why isn't there an API for this? + if (this.editor.form.$element.hasClass('oe_form_dirty')) { + this.save_edition(); + return; + } + this.cancel_edition(); + }, + keyup_ENTER: function () { + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing (save + // the current row *anyway*, then create a new one/edit the next one) + this.__ignore_blur = true; + this._super.apply(this, arguments); } }); @@ -3545,7 +3577,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.addClass('oe_form_field_many2many'); + this.$element.addClass('oe_form_field oe_form_field_many2many'); var self = this; @@ -4079,10 +4111,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.dataset, false, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, - 'read_only': true, 'import_enabled': false, '$buttons': self.$buttonpane, }, self.options.list_view_options || {})); + self.view_list.on('edit:before', self, function (e) { + e.cancel = true; + }); self.view_list.popup = self; self.view_list.appendTo($(".oe_popup_list", self.$element)).pipe(function() { self.view_list.do_show(); @@ -4314,7 +4348,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. //link.target = '_blank'; link.href = "data:application/octet-stream;base64," + value; } else { - $.blockUI(); + instance.web.blockUI(); this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ @@ -4324,7 +4358,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. filename_field: (this.node.attrs.filename || ''), context: this.view.dataset.get_context() })}, - complete: $.unblockUI, + complete: instance.web.unblockUI, error: instance.webclient.crashmanager.on_rpc_error }); ev.stopPropagation(); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index edc53f82042..53a8c0126b2 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi // whether the view rows can be reordered (via vertical drag & drop) 'reorderable': true, 'action_buttons': true, - // if true, the view can't be editable, ignoring the view's and the context's - // instructions - 'read_only': false, // if true, the 'Import', 'Export', etc... buttons will be shown 'import_enabled': true, }, @@ -142,7 +139,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); }, /** - * View startup method, the default behavior is to set the ``oe_listw`` + * View startup method, the default behavior is to set the ``oe_list`` * class on its root element and to perform an RPC load call. * * @returns {$.Deferred} loading promise @@ -288,7 +285,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } this.$buttons.find('.oe_list_add') .click(this.proxy('do_add_record')) - .prop('disabled', grouped && this.options.editable); + .prop('disabled', grouped); this.$buttons.on('click', '.oe_list_button_import', function() { self.on_sidebar_import(); return false; @@ -411,20 +408,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (column.modifiers) { var modifiers = JSON.parse(column.modifiers); column.modifiers_for = function (fields) { - if (!modifiers.invisible) { - return {}; + var out = {}; + + for (var attr in modifiers) { + if (!modifiers.hasOwnProperty(attr)) { continue; } + var modifier = modifiers[attr]; + out[attr] = _.isBoolean(modifier) + ? modifier + : domain_computer(modifier, fields); } - return { - 'invisible': domain_computer(modifiers.invisible, fields) - }; + + return out; }; if (modifiers['tree_invisible']) { column.invisible = '1'; } else { delete column.invisible; } + column.modifiers = modifiers; } else { column.modifiers_for = noop; + column.modifiers = {}; } return column; }; @@ -436,10 +440,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (grouped) { this.columns.unshift({ id: '_group', tag: '', string: _t("Group"), meta: true, - modifiers_for: function () { return {}; } + modifiers_for: function () { return {}; }, + modifiers: {} }, { id: '_count', tag: '', string: '#', meta: true, - modifiers_for: function () { return {}; } + modifiers_for: function () { return {}; }, + modifiers: {} }); } @@ -667,6 +673,19 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + this.handle_button(name, id, callback); + }, + /** + * Base handling of buttons, can be called when overriding do_button_action + * in order to bypass parent overrides. + * + * This method should not be overridden. + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + handle_button: function (name, id, callback) { var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -911,27 +930,42 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.record_callbacks = { 'remove': function (event, record) { - var $row = self.$current.find( - '[data-id=' + record.get('id') + ']'); + var $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); var index = $row.data('index'); $row.remove(); self.refresh_zebra(index); }, 'reset': function () { return self.on_records_reset(); }, - 'change': function (event, record) { - var $row = self.$current.find('[data-id=' + record.get('id') + ']'); + 'change': function (event, record, attribute, value, old_value) { + var $row; + if (attribute === 'id') { + if (old_value) { + throw new Error("Setting 'id' attribute on existing record " + + JSON.stringify(record.attributes)); + } + if (!_.contains(self.dataset.ids, value)) { + // add record to dataset if not already in (added by + // the form view?) + self.dataset.ids.splice( + self.records.indexOf(record), 0, value); + } + // Set id on new record + $row = self.$current.children('[data-id=false]'); + } else { + $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); + } $row.replaceWith(self.render_record(record)); }, 'add': function (ev, records, record, index) { - var $new_row = $('<tr>').attr({ - 'data-id': record.get('id') - }); + var $new_row = $(self.render_record(record)); if (index === 0) { $new_row.prependTo(self.$current); } else { var previous_record = records.at(index-1), - $previous_sibling = self.$current.find( + $previous_sibling = self.$current.children( '[data-id=' + previous_record.get('id') + ']'); $new_row.insertAfter($previous_sibling); } @@ -975,11 +1009,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. e.stopPropagation(); }) .delegate('tr', 'click', function (e) { - e.stopPropagation(); var row_id = self.row_id(e.currentTarget); - if (row_id !== undefined) { + if (row_id) { + e.stopPropagation(); if (!self.dataset.select_id(row_id)) { - throw "Could not find id in dataset" + throw new Error("Could not find id in dataset"); } self.row_clicked(e); } @@ -1139,6 +1173,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {String} QWeb rendering of the selected record */ render_record: function (record) { + var self = this; var index = this.records.indexOf(record); return QWeb.render('ListView.row', { columns: this.columns, @@ -1147,7 +1182,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. row_parity: (index % 2 === 0) ? 'even' : 'odd', view: this.view, render_cell: function () { - return this.render_cell.apply(this, arguments); } + return self.render_cell.apply(self, arguments); } }); }, /** @@ -1831,7 +1866,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ * @returns this */ remove: function (record) { - var index = _(this.records).indexOf(record); + var index = this.indexOf(record); if (index === -1) { _(this._proxies).each(function (proxy) { proxy.remove(record); @@ -1847,13 +1882,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ return this; }, - _onRecordEvent: function (event, record, options) { + _onRecordEvent: function (event) { + switch(event) { // don't propagate reset events - if (event === 'reset') { return; } + case 'reset': return; + case 'change:id': + var record = arguments[1]; + var new_value = arguments[2]; + var old_value = arguments[3]; + // [change:id, record, new_value, old_value] + if (this._byId[old_value] === record) { + delete this._byId[old_value]; + this._byId[new_value] = record; + } + break; + } this.trigger.apply(this, arguments); }, // underscore-type methods + find: function (callback) { + var record; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue + } + if ((record = this._proxies[section].find(callback))) { + return record; + } + } + for(var i=0; i<this.length; ++i) { + record = this.records[i]; + if (callback(record)) { + return record; + } + } + }, each: function (callback) { for(var section in this._proxies) { if (this._proxies.hasOwnProperty(section)) { @@ -1878,6 +1942,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ }, indexOf: function (record) { return _(this.records).indexOf(record); + }, + succ: function (record, options) { + options = options || {wraparound: false}; + var result; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].succ(record, options))) { + return result; + } + } + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index + 1; + if (options.wraparound && (next_index === this.length)) { + return this.at(0); + } + return this.at(next_index); + }, + pred: function (record, options) { + options = options || {wraparound: false}; + + var result; + for (var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].pred(record, options))) { + return result; + } + } + + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index - 1; + if (options.wraparound && (next_index === -1)) { + return this.at(this.length - 1); + } + return this.at(next_index); } }); Collection.include(Events); diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index c370aeaf7fd..c7654479864 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -3,10 +3,6 @@ * @namespace */ openerp.web.list_editable = function (instance) { - var KEY_RETURN = 13, - KEY_ESCAPE = 27; - var QWeb = instance.web.qweb; - // editability status of list rows instance.web.ListView.prototype.defaults.editable = null; @@ -15,6 +11,15 @@ openerp.web.list_editable = function (instance) { init: function () { var self = this; this._super.apply(this, arguments); + + this._force_editability = null; + this._context_editable = false; + this.editor = this.make_editor(); + // Stores records of {field, cell}, allows for re-rendering fields + // depending on cell state during and after resize events + this.fields_for_resize = []; + instance.web.bus.on('resize', this, this.resize_fields); + $(this.groups).bind({ 'edit': function (e, id, dataset) { self.do_edit(dataset.index, id, dataset); @@ -26,7 +31,29 @@ openerp.web.list_editable = function (instance) { self.configure_pager(self.dataset); self.compute_aggregates(); } - }) + }); + + this.records.bind('remove', function () { + if (self.editor.is_editing()) { + self.cancel_edition(); + } + }); + + this.on('edit:before', this, function (event) { + if (!self.editable() || self.editor.is_editing()) { + event.cancel = true; + } + }); + this.on('edit:after', this, function () { + self.$element.add(self.$buttons).addClass('oe_editing'); + }); + this.on('save:after cancel:after', this, function () { + self.$element.add(self.$buttons).removeClass('oe_editing'); + }); + }, + destroy: function () { + instance.web.bus.off('resize', this, this.resize_fields); + this._super(); }, /** * Handles the activation of a record in editable mode (making a record @@ -42,25 +69,16 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, - /** - * Sets editability status for the list, based on defaults, view - * architecture and the provided flag, if any. - * - * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". - */ - set_editable: function (force) { - // If ``force``, set editability to bottom - // otherwise rely on view default - // view' @editable is handled separately as we have not yet - // fetched and processed the view at this point. - this.options.editable = ( - ! this.options.read_only && ((force && "bottom") || this.defaults.editable)); + editable: function () { + return this.fields_view.arch.attrs.editable + || this._context_editable + || this.options.editable; }, /** * Replace do_search to handle editability process */ do_search: function(domain, context, group_by) { - this.set_editable(context['set_editable']); + this._context_editable = !!context.set_editable; this._super.apply(this, arguments); }, /** @@ -68,18 +86,62 @@ openerp.web.list_editable = function (instance) { * as an editable row at the top or bottom of the list) */ do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this.$element.find('table:first').show(); this.$element.find('.oe_view_nocontent').remove(); - this.groups.new_record(); + this.start_edition(); } else { this._super(); } }, on_loaded: function (data, grouped) { + var self = this; + if (this.editor) { + this.editor.destroy(); + } // tree/@editable takes priority on everything else if present. - this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable); - return this._super(data, grouped); + var result = this._super(data, grouped); + if (this.editable()) { + // FIXME: any hook available to ensure this is only done once? + this.$buttons + .off('click', '.oe_list_save') + .on('click', '.oe_list_save', this.proxy('save_edition')) + .off('click', '.oe_list_discard') + .on('click', '.oe_list_discard', function (e) { + e.preventDefault(); + self.cancel_edition(); + }); + this.$element + .off('click', 'tbody td:not(.oe_list_field_cell)') + .on('click', 'tbody td:not(.oe_list_field_cell)', function () { + if (!self.editor.is_editing()) { + self.start_edition(); + } + }); + // Editor is not restartable due to formview not being + // restartable + this.editor = this.make_editor(); + var editor_ready = this.editor.prependTo(this.$element) + .then(this.proxy('setup_events')); + + return $.when(result, editor_ready); + } + + return result; + }, + /** + * Builds a new editor object + * + * @return {instance.web.list.Editor} + */ + make_editor: function () { + return new instance.web.list.Editor(this); + }, + do_button_action: function () { + var self = this, args = arguments; + this.ensure_saved().then(function () { + self.handle_button.apply(self, args); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -90,354 +152,581 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - return this.groups.ensure_saved(); - } - }); - - instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", - new_record: function () { - // TODO: handle multiple children - this.children[null].new_record(); + if (!this.editor.is_editing()) { + return $.when(); + } + return this.save_edition(); }, /** - * Ensures descendant editable List instances are all saved if they have - * pending editions. + * Set up the edition of a record of the list view "inline" * - * @returns {$.Deferred} + * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record + * @param {Object} [options] + * @param {String} [options.focus_field] field to focus at start of edition + * @return {jQuery.Deferred} */ - ensure_saved: function () { - return $.when.apply(null, - _.invoke( - _.values(this.children), - 'ensure_saved')); - } - }); - - instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ - row_clicked: function (event) { - if (!this.options.editable) { - return this._super.apply(this, arguments); + start_edition: function (record, options) { + var self = this; + var item = false; + if (record) { + item = record.attributes; + } else { + var attrs = {id: false}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + record = new instance.web.list.Record(attrs); + this.records.add(record, { + at: this.prepends_on_create() ? 0 : null}); } - this.edit_record($(event.currentTarget).data('id')); + var $recordRow = this.groups.get_row_for(record); + var cells = this.get_cells_for($recordRow); + + return this.ensure_saved().pipe(function () { + self.fields_for_resize.splice(0, self.fields_for_resize.length); + return self.with_event('edit', { + record: record.attributes, + cancel: false + }, function () { + return self.editor.edit(item, function (field_name, field) { + var cell = cells[field_name]; + if (!cell || field.get('effective_readonly')) { + // Readonly fields can just remain the list's, + // form's usually don't have backgrounds &al + field.set({invisible: true}); + return; + } + + // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow + field.$element.attr('data-fieldname', field_name); + self.fields_for_resize.push({field: field, cell: cell}); + }, options).pipe(function () { + $recordRow.addClass('oe_edition'); + self.resize_fields(); + return record.attributes; + }); + }).fail(function () { + // if the start_edition event is cancelled and it was a + // creation, remove the newly-created empty record + if (!record.get('id')) { + self.records.remove(record); + } + }); + }); + }, + get_cells_for: function ($row) { + var cells = {}; + $row.children('td').each(function (index, el) { + cells[el.getAttribute('data-field')] = el + }); + return cells; }, /** - * Checks if a record is being edited, and if so cancels it + * If currently editing a row, resizes all registered form fields based + * on the corresponding row cell */ - cancel_pending_edition: function () { - var self = this, cancelled; - if (!this.edition) { - return $.when(); + resize_fields: function () { + if (!this.editor.is_editing()) { return; } + for(var i=0, len=this.fields_for_resize.length; i<len; ++i) { + var item = this.fields_for_resize[i]; + this.resize_field(item.field, item.cell); } - - if (this.edition_id) { - cancelled = this.reload_record(this.records.get(this.edition_id)); - } else { - cancelled = $.when(); - } - cancelled.then(function () { - self.view.unpad_columns(); - self.edition_form.destroy(); - self.edition_form.$element.remove(); - delete self.edition_form; - self.dataset.index = null; - delete self.edition_id; - delete self.edition; - }); - this.pad_table_to(5); - return cancelled; }, /** - * Adapts this list's view description to be suitable to the inner form - * view of a row being edited. + * Resizes a field's root element based on the corresponding cell of + * a listview row * - * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + * @param {instance.web.form.AbstractField} field + * @param {jQuery} cell */ - get_form_fields_view: function () { - // deep copy of view - var view = $.extend(true, {}, this.group.view.fields_view); - _(view.arch.children).each(function (widget) { - widget.attrs.nolabel = true; - if (widget.tag === 'button') { - delete widget.attrs.string; - } + resize_field: function (field, cell) { + var $cell = $(cell); + var position = $cell.position(); + + field.$element.css({ + top: position.top, + left: position.left, + width: $cell.outerWidth(), + minHeight: $cell.outerHeight() }); - view.arch.attrs.col = 2 * view.arch.children.length; - return view; }, - on_row_keyup: function (e) { + /** + * @return {jQuery.Deferred} + */ + save_edition: function () { var self = this; - switch (e.which) { - case KEY_RETURN: - $(e.target).blur(); - e.preventDefault(); - //e.stopImmediatePropagation(); - setTimeout(function () { - self.save_row().then(function (result) { - if (result.created) { - self.new_record(); - return; - } - - var next_record_id, - next_record = self.records.at( - self.records.indexOf(result.edited_record) + 1); - if (next_record) { - next_record_id = next_record.get('id'); - self.dataset.index = _(self.dataset.ids) - .indexOf(next_record_id); - } else { - self.dataset.index = 0; - next_record_id = self.records.at(0).get('id'); - } - self.edit_record(next_record_id); - }, 0); + return this.with_event('save', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.save().pipe(function (attrs) { + var created = false; + var record = self.records.get(attrs.id); + if (!record) { + // new record + created = true; + record = self.records.find(function (r) { + return !r.get('id'); + }).set('id', attrs.id); + } + // onwrite callback could be altering & reloading the + // record which has *just* been saved, so first perform all + // onwrites then do a final reload of the record + return self.handle_onwrite(record) + .pipe(function () { + return self.reload_record(record); }) + .pipe(function () { + return { created: created, record: record }; }); }); - break; - case KEY_ESCAPE: - this.cancel_edition(); - break; - } + }); }, - render_row_as_form: function (row) { + /** + * @return {jQuery.Deferred} + */ + cancel_edition: function () { var self = this; - return this.ensure_saved().pipe(function () { - var record_id = $(row).data('id'); - var $new_row = $('<tr>', { - id: _.uniqueId('oe-editable-row-'), - 'data-id': record_id, - 'class': (row ? $(row).attr('class') : ''), - click: function (e) {e.stopPropagation();} - }) - .addClass('oe_form oe_form_container') - .delegate('button.oe_list_edit_row_save', 'click', function () { - self.save_row(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function () { - return self.on_row_keyup.apply(self, arguments); }) - .keydown(function (e) { e.stopPropagation(); }) - .keypress(function (e) { - if (e.which === KEY_RETURN) { - return false; - } - }); - - if (row) { - $new_row.replaceAll(row); - } else if (self.options.editable) { - var $last_child = self.$current.children('tr:last'); - if (self.records.length) { - if (self.options.editable === 'top') { - $new_row.insertBefore( - self.$current.children('[data-id]:first')); - } else { - $new_row.insertAfter( - self.$current.children('[data-id]:last')); + return this.with_event('cancel', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.cancel().pipe(function (attrs) { + if (attrs.id) { + var record = self.records.get(attrs.id); + if (!record) { + // Record removed by third party during edition + return } - } else { - $new_row.prependTo(self.$current); + return self.reload_record(record); } - if ($last_child.is(':not([data-id])')) { - $last_child.remove(); - } - } - self.edition = true; - self.edition_id = record_id; - self.dataset.index = _(self.dataset.ids).indexOf(record_id); - if (self.dataset.index === -1) { - self.dataset.index = null; - } - self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), { - $element: $new_row, - editable_list: self - }); - // put in $.when just in case FormView.on_loaded becomes asynchronous - return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - $new_row.find('> td') - .end() - .find('td:last').removeClass('oe_list_field_cell').end(); - // pad in case of groupby - _(self.columns).each(function (column) { - if (column.meta) { - $new_row.prepend('<td>'); - } + var to_delete = self.records.find(function (r) { + return !r.get('id'); }); - // Add column for the save, if - // there is none in the list - if (!self.options.deletable) { - self.view.pad_columns( - 1, {except: $new_row}); - } - - self.edition_form.do_show(); - }); - }); - }, - handle_onwrite: function (source_record_id) { - var self = this; - var on_write_callback = self.view.fields_view.arch.attrs.on_write; - if (!on_write_callback) { return; } - this.dataset.call(on_write_callback, [source_record_id], function (ids) { - _(ids).each(function (id) { - var record = self.records.get(id); - if (!record) { - // insert after the source record - var index = self.records.indexOf( - self.records.get(source_record_id)) + 1; - record = new instance.web.list.Record({id: id}); - self.records.add(record, {at: index}); - self.dataset.ids.splice(index, 0, id); + if (to_delete) { + self.records.remove(to_delete); } - self.reload_record(record); }); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: + * Executes an action on the view's editor bracketed by a cancellable + * event of the name provided. * - * ``created`` - * Boolean flag indicating whether the record saved was being created - * (``true`` or edited (``false``) - * ``edited_record`` - * The result of saving the record (either the newly created record, - * or the post-edition record), after insertion in the Collection if - * needs be. + * The event name provided will be post-fixed with ``:before`` and + * ``:after``, the ``event`` parameter will be passed alongside the + * ``:before`` variant and if the parameter's ``cancel`` key is set to + * ``true`` the action *will not be called* and the method will return + * a rejection * - * @returns {$.Deferred<{created: Boolean, edited_record: Record}>} + * @param {String} event_name name of the event + * @param {Object} event event object, provided to ``:before`` sub-event + * @param {Function} action callable, called with the view's editor as its context + * @param {Array} [args] supplementary arguments provided to the action + * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function + * @return {jQuery.Deferred} */ - save_row: function () { - //noinspection JSPotentiallyInvalidConstructorUsage + with_event: function (event_name, event, action) { var self = this; - return this.edition_form - .do_save(null, this.options.editable === 'top') - .pipe(function (result) { - if (result.created && !self.edition_id) { - self.records.add({id: result.result}, - {at: self.options.editable === 'top' ? 0 : null}); - self.edition_id = result.result; - } - var edited_record = self.records.get(self.edition_id); - - return $.when( - self.handle_onwrite(self.edition_id), - self.cancel_pending_edition().then(function () { - $(self).trigger('saved', [self.dataset]); - })).pipe(function () { - return { - created: result.created || false, - edited_record: edited_record - }; - }); + event = event || {}; + this.trigger(event_name + ':before', event); + if (event.cancel) { + return $.Deferred().reject({ + message: _.str.sprintf("Event %s:before cancelled", + event_name)}); + } + return $.when(action.call(this)).then(function () { + self.trigger.apply(self, [event_name + ':after'] + .concat(_.toArray(arguments))); + }); + }, + edition_view: function (editor) { + var view = $.extend(true, {}, this.fields_view); + view.arch.tag = 'form'; + _.extend(view.arch.attrs, { + 'class': 'oe_form_container', + version: '7.0' + }); + _(view.arch.children).each(function (widget) { + var modifiers = JSON.parse(widget.attrs.modifiers || '{}'); + widget.attrs.nolabel = true; + if (modifiers['tree_invisible'] || widget.tag === 'button') { + modifiers.invisible = true; + } + widget.attrs.modifiers = JSON.stringify(modifiers); + }); + return view; + }, + handle_onwrite: function (source_record) { + var self = this; + var on_write_callback = self.fields_view.arch.attrs.on_write; + if (!on_write_callback) { return $.when(); } + return this.dataset.call(on_write_callback, [source_record.get('id')]) + .pipe(function (ids) { + return $.when.apply( + null, _(ids).map( + _.bind(self.handle_onwrite_record, self, source_record))); }); }, + handle_onwrite_record: function (source_record, id) { + var record = this.records.get(id); + if (!record) { + // insert after the source record + var index = this.records.indexOf(source_record) + 1; + record = new instance.web.list.Record({id: id}); + this.records.add(record, {at: index}); + this.dataset.ids.splice(index, 0, id); + } + return this.reload_record(record); + }, + prepends_on_create: function () { + return this.editable() === 'top'; + }, + setup_events: function () { + var self = this; + this.editor.$element.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return; } + var key = _($.ui.keyCode).chain() + .map(function (v, k) { return {name: k, code: v}; }) + .find(function (o) { return o.code === e.which; }) + .value(); + if (!key) { return; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return; } + self[method](e); + }); + }, /** - * If the current list is being edited, ensures it's saved + * Saves the current record, and goes to the next one (creation or + * edition) + * + * @private + * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit + * @param {Object} [options] + * @param {String} [options.focus_field] + * @return {*} */ - ensure_saved: function () { - if (this.edition) { - // kinda-hack-ish: if the user has entered data in a field, - // oe_form_dirty will be set on the form so save, otherwise - // discard the current (entirely empty) line - if (this.edition_form.$element.is('.oe_form_dirty')) { - return this.save_row(); + _next: function (next_record, options) { + next_record = next_record || 'succ'; + var self = this; + return this.save_edition().pipe(function (saveInfo) { + if (saveInfo.created) { + return self.start_edition(); + } + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); + }); + }, + keyup_ENTER: function () { + return this._next(); + }, + keyup_ESCAPE: function () { + return this.cancel_edition(); + }, + _text_selection_range: function (el) { + if (el.selectionStart !== undefined) { + return { + start: el.selectionStart, + end: el.selectionEnd + }; + } else if(document.body.createTextRange) { + throw new Error("Implement text range handling for MSIE"); + var sel = document.body.createTextRange(); + if (sel.parentElement() === el) { + } - return this.cancel_pending_edition(); } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.when(); }, - /** - * Cancels the edition of the row for the current dataset index - */ - cancel_edition: function () { - this.cancel_pending_edition(); + _text_cursor: function (el) { + var selection = this._text_selection_range(el); + if (selection.start !== selection.end) { + return null; + } + return selection.start; }, /** - * Edits record currently selected via dataset + * @param DOMEvent event + * @param {String} record_direction direction to move into to get the next record (pred | succ) + * @param {Function} is_valid_move whether the edition should be moved to the next record + * @private */ - edit_record: function (record_id) { - this.render_row_as_form( - this.$current.find('[data-id=' + record_id + ']')); - $(this).trigger( - 'edit', - [record_id, this.dataset]); - }, - new_record: function () { - this.render_row_as_form(); - }, - render_record: function (record) { - var index = this.records.indexOf(record), - self = this; - // FIXME: context dict should probably be extracted cleanly - return QWeb.render('ListView.row', { - columns: this.columns, - options: this.options, - record: record, - row_parity: (index % 2 === 0) ? 'even' : 'odd', - view: this.view, - render_cell: function () { - return self.render_cell.apply(self, arguments); }, - edited: !!this.edition_form + _key_move_record: function (event, record_direction, is_valid_move) { + if (!this.editor.is_editing('edit')) { return $.when(); } + // FIXME: assumes editable widgets are input-type elements + var index = this._text_cursor(event.target); + // If selecting or not at the start of the input + if (!is_valid_move(event.target, index)) { return $.when(); } + + event.preventDefault(); + var source_field = $(event.target).closest('[data-fieldname]') + .attr('data-fieldname'); + return this._next(record_direction, {focus_field: source_field}); + + }, + keydown_UP: function (e) { + return this._key_move_record(e, 'pred', function (el, index) { + return index === 0; + }); + }, + keydown_DOWN: function (e) { + return this._key_move_record(e, 'succ', function (el, index) { + return index === el.value.length; }); + }, + + keydown_LEFT: function (e) { + // If the cursor is at the beginning of the field + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var index = this._text_cursor(e.target); + if (index !== 0) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + // Look for the closest visible form field to the left + var fields = this.editor.form.fields; + var field; + do { + if (--field_index < 0) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$element.is(':visible')); + + // and focus it + field.focus(); + return $.when(); + }, + keydown_RIGHT: function (e) { + // same as above, but with cursor at the end of the field and + // looking for new fields at the right + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var index = this._text_cursor(e.target); + if (index !== e.target.value.length) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + var fields = this.editor.form.fields; + var field; + do { + if (++field_index >= fields_order.length) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$element.is(':visible')); + + field.focus(); + return $.when(); + }, + keydown_TAB: function (e) { + var form = this.editor.form; + var last_field = _(form.fields_order).chain() + .map(function (name) { return form.fields[name]; }) + .filter(function (field) { return field.$element.is(':visible'); }) + .last() + .value(); + // tabbed from last field in form + if (last_field && last_field.$element.has(e.target).length) { + e.preventDefault(); + return this._next(); + } + return $.when(); } }); - - instance.web.ListEditableFormView = instance.web.FormView.extend({ - init: function() { - this._super.apply(this, arguments); - this.rendering_engine = new instance.web.ListEditableRenderingEngine(this); - this.options.initial_mode = "edit"; + + instance.web.list.Editor = instance.web.Widget.extend({ + /** + * @constructs instance.web.list.Editor + * @extends instance.web.Widget + * + * Adapter between listview and formview for editable-listview purposes + * + * @param {instance.web.Widget} parent + * @param {Object} options + * @param {instance.web.FormView} [options.formView=instance.web.FormView] + * @param {Object} [options.delegate] + */ + init: function (parent, options) { + this._super(parent); + this.options = options || {}; + _.defaults(this.options, { + formView: instance.web.FormView, + delegate: this.getParent() + }); + this.delegate = this.options.delegate; + + this.record = null; + + this.form = new (this.options.formView)( + this, this.delegate.dataset, false, { + initial_mode: 'edit', + $buttons: $(), + $pager: $() + }); }, - renderElement: function() {} - }); - - instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ - init: function(view) { - this.view = view; + start: function () { + var self = this; + var _super = this._super(); + this.form.embedded_view = this._validate_view( + this.delegate.edition_view(this)); + var form_ready = this.form.appendTo(this.$element).then( + self.form.proxy('do_hide')); + return $.when(_super, form_ready); }, - set_fields_view: function(fields_view) { - this.fvg = fields_view; + _validate_view: function (edition_view) { + if (!edition_view) { + throw new Error("editor delegate's #edition_view must return " + + "a view descriptor"); + } + var arch = edition_view.arch; + if (!(arch && arch.children instanceof Array)) { + throw new Error("Editor delegate's #edition_view must have a" + + " non-empty arch") + } + if (!(arch.tag === "form")) { + throw new Error("Editor delegate's #edition_view must have a" + + " 'form' root node"); + } + if (!(arch.attrs && arch.attrs.version === "7.0")) { + throw new Error("Editor delegate's #edition_view must be a" + + " version 7 view"); + } + if (!/\boe_form_container\b/.test(arch.attrs['class'])) { + throw new Error("Editor delegate's #edition_view must have the" + + " class 'oe_form_container' on its root" + + " element"); + } + + return edition_view; }, - set_tags_registry: function(tags_registry) { - this.tags_registry = tags_registry; + + /** + * + * @param {String} [state] either ``new`` or ``edit`` + * @return {Boolean} + */ + is_editing: function (state) { + if (!this.record) { + return false; + } + switch(state) { + case null: case undefined: + return true; + case 'new': return !this.record.id; + case 'edit': return !!this.record.id; + } + throw new Error("is_editing's state filter must be either `new` or" + + " `edit` if provided"); }, - set_fields_registry: function(fields_registry) { - this.fields_registry = fields_registry; + _focus_setup: function (focus_field) { + var form = this.form; + + var field; + // If a field to focus was specified + if (focus_field + // Is actually in the form + && (field = form.fields[focus_field]) + // And is visible + && field.$element.is(':visible')) { + // focus it + field.focus(); + return; + } + + _(form.fields_order).detect(function (name) { + // look for first visible field in fields_order, focus it + var field = form.fields[name]; + if (!field.$element.is(':visible')) { + return false; + } + field.focus(); + // Stop as soon as a field got focused + return true; + }); }, - render_to: function($element) { + edit: function (record, configureField, options) { + // TODO: specify sequence of edit calls var self = this; - - var xml = instance.web.json_node_to_xml(this.fvg.arch); - var $xml = $(xml); - - if (this.view.editable_list.options.selectable) - $("<td>").appendTo($element); - - $xml.children().each(function(i, el) { - var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); - var $td = $("<td>"); - if (modifiers.tree_invisible === true) - $td.hide(); - var tag_name = el.tagName.toLowerCase(); - var w; - if (tag_name === "field") { - var name = $(el).attr("name"); - var key = $(el).attr('widget') || self.fvg.fields[name].type; - var obj = self.view.fields_registry.get_object(key); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - self.view.register_field(w, $(el).attr("name")); - } else { - var obj = self.tags_registry.get_object(tag_name); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - } - w.appendTo($td); - $td.appendTo($element); + var form = self.form; + var loaded = record + ? form.on_record_loaded(_.extend({}, record)) + : form.load_defaults(); + + return loaded.pipe(function () { + return form.do_show({reload: false}); + }).pipe(function () { + self.record = form.datarecord; + _(form.fields).each(function (field, name) { + configureField(name, field); + }); + self._focus_setup(options && options.focus_field); + return form; + }); + }, + save: function () { + var self = this; + return this.form + .do_save(null, this.delegate.prepends_on_create()) + .pipe(function (result) { + var created = result.created && !self.record.id; + if (created) { + self.record.id = result.result; + } + return self.cancel(); + }); + }, + cancel: function () { + var record = this.record; + this.record = null; + if (!this.form.can_be_discarded()) { + return $.Deferred().reject({ + message: "The form's data can not be discarded"}).promise(); + } + this.form.do_hide(); + return $.when(record); + } + }); + + instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ + passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", + get_row_for: function (record) { + return _(this.children).chain() + .invoke('get_row_for', record) + .compact() + .first() + .value(); + } + }); + + instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ + row_clicked: function (event) { + if (!this.view.editable()) { + return this._super.apply(this, arguments); + } + var record_id = $(event.currentTarget).data('id'); + this.view.start_edition( + record_id ? this.records.get(record_id) : null, { + focus_field: $(event.target).data('field') }); - $(QWeb.render('ListView.row.save')).appendTo($element); }, + /** + * If a row mapping to the record (@data-id matching the record's id or + * no @data-id if the record has no id), returns it. Otherwise returns + * ``null``. + * + * @param {Record} record the record to get a row for + * @return {jQuery|null} + */ + get_row_for: function (record) { + var id; + var $row = this.$current.children('[data-id=' + record.get('id') + ']'); + if ($row.length) { + return $row; + } + return null; + } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index bd102d867de..ddf4050fa38 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -274,7 +274,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ }, ir_actions_report_xml: function(action, on_closed) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -284,7 +284,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, - complete: $.unblockUI, + complete: instance.web.unblockUI, success: function(){ if (!self.dialog && on_closed) { on_closed(); @@ -841,7 +841,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ } else { self.do_attachement_update(self.dataset, self.model_id); } - $.unblockUI(); + instance.web.unblockUI(); }); }, start: function() { @@ -980,7 +980,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ $e.parent().find('input[type=file]').prop('disabled', true); $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...')); - $.blockUI(); + instance.web.blockUI(); } }, on_attachment_delete: function(e) { @@ -1074,11 +1074,13 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ if (self.view.translatable_fields && self.view.translatable_fields.length) { self.do_load_fields_values(function() { sup.call(self); + // desactivated because it created an exception, plus it does not seem very useful + /* if (field) { var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child'); self.$element.scrollTo($field_input); $field_input.focus(); - } + }*/ }); } else { sup.call(self); @@ -1346,7 +1348,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { if (typeof(node) === 'string') { return sindent + node; } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) { - throw("Node a json node"); + throw new Error( + _.str.sprintf("Node [%s] is not a JSONified XML node", + JSON.stringify(node))); } for (var attr in node.attrs) { var vattr = node.attrs[attr]; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 55280f6badb..5d55a0ece15 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -329,21 +329,20 @@ </span> </t> <t t-name="UserMenu.about"> - <div> - <a class="oe_activate_debug_mode" href="?debug" style="float:right; font-size: 80%;">Activate the developer mode</a> - <h1 style="margin:0;">OpenERP</h1> - <h3 style="margin:15px 0;padding:0;">Version <t t-esc="version_info.version"/></h3> - <p> - Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br /> - OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>. - </p> - <p> - Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a> - </p> - <p> - For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a> - </p> + + <div class="oe_about"> + <a class="oe_activate_debug_mode oe_right" href="?debug">Activate the developer mode</a> + <img class="oe_logo" src="/web/static/src/img/logo2.png"/> + <h3>Version <t t-esc="version_info.version"/></h3> + + <div class="oe_bottom"> + <p>Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br /> + OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>.</p> + <p>Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a></p> + <p>For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a></p> + </div> </div> + </t> <t t-name="UserMenu.password"> <form name="change_password_form" method="POST"> @@ -630,9 +629,9 @@ <button type="button" class="oe_button oe_list_add oe_highlight"> <t t-esc="widget.options.addable"/> </button> - <t t-if="widget.options.import_enabled"> + <span class="oe_alternative" t-if="widget.options.import_enabled"> <span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a> - </t> + </span> </t> </div> <t t-name="ListView.pager"> @@ -652,6 +651,7 @@ <tr t-name="ListView.row" t-att-class="row_parity" t-att-data-id="record.get('id')" t-att-style="view.style_for(record)"> + <t t-set="asData" t-value="record.toForm().data"/> <t t-foreach="columns" t-as="column"> <td t-if="column.meta"> @@ -663,23 +663,26 @@ <input t-if="!options.radio" type="checkbox" name="radiogroup" t-att-checked="checked"/> </th> <t t-foreach="columns" t-as="column"> - <t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/> + <t t-set="number" t-value="column.type === 'integer' or column.type == 'float'"/> + <t t-set="modifiers" t-value="column.modifiers_for(asData)"/> <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help" - t-att-class="'oe_list_field_cell' + (align ? ' oe_number' : '') - + (column.tag === 'button' ? ' oe_button' : '')" - t-att-data-field="column.id"> - <t t-raw="render_cell(record, column)"/> - </td> + t-attf-class="oe_list_field_cell oe_list_field_#{column.widget or column.type} #{number ? 'oe_number' : ''} #{column.tag === 'button' ? 'oe-button' : ''} #{modifiers.readonly ? 'oe_readonly' : ''}" + t-att-data-field="column.id" + ><t t-raw="render_cell(record, column)"/></td> </t> <td t-if="options.deletable" class='oe_list_record_delete' width="1"> <button type="button" name="delete" class="oe_i">d</button> </td> </tr> -<t t-name="ListView.row.save"> - <td> - <button class='oe_i oe_list_edit_row_save' type='button' name='save'/> - </td> +<t t-extend="ListView.buttons"> + <t t-jquery="button.oe_list_add" t-operation="after"> + <button class="oe_button oe_list_save oe_highlight" + type="button">Save</button> + </t> + <t t-jquery="a.oe_list_button_import" t-operation="after"> + <a href="#" class="oe_bold oe_list_discard">discard</a> + </t> </t> <t t-name="FormView"> @@ -956,7 +959,7 @@ <span class="oe_form_m2o_follow"/> </t> <t t-if="!widget.get('effective_readonly')"> - <a href="#" class="oe_m2o_cm_button oe_e oe_right">/</a> + <a href="#" tabindex="-1" class="oe_m2o_cm_button oe_e oe_right">/</a> <div> <input type="text" t-att-id="widget.id_for_label" @@ -1216,6 +1219,8 @@ <div class="oe_searchview_clear"/> <div class="oe_searchview_unfold_drawer" title="Advanced Search..."/> <div class="oe_searchview_drawer"/> + <button type="button" class="oe_searchview_search" + title="Search Again">Search</button> </div> <div t-name="SearchView.InputView" @@ -1403,8 +1408,8 @@ <div t-name="SearchView.addtodashboard" class="oe_searchview_dashboard"> <h4>Add to Dashboard</h4> <form> - <input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/> - <button class="oe_apply" type="submit">save</button> + <p><input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/></p> + <button class="oe_apply" type="submit">Save</button> </form> </div> <t t-name="SearchView.addtodashboard.selection"> @@ -1498,7 +1503,7 @@ missing columns --> <t t-jquery="> :last" t-operation="after"> - <td t-if="edited and !options.deletable" class="oe_list_padding"/> + <td t-if="edited and !options.deletable" class="oe-listview-padding"/> </t> </t> |
