diff options
Diffstat (limited to 'addons/web/static/src/js')
15 files changed, 184 insertions, 58 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 9566dbda850..6b1a6249962 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -33,7 +33,9 @@ var qweb = core.qweb; var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, { dependencies: ['notification'], - events: _.extend(KeyboardNavigationMixin.events, {}), + events: _.extend({}, KeyboardNavigationMixin.events, { + 'click .o_search_options .dropdown-menu': '_onClickDropDownMenu', + }), custom_events: { clear_uncommitted_changes: function (e) { this.clear_uncommitted_changes().then(e.data.callback); @@ -356,6 +358,16 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi //-------------------------------------------------------------------------- /** + * When clicking inside a dropdown to modify search options + * prevents the bootstrap dropdown to close on itself + * + * @private + * @param {Event} ev + */ + _onClickDropDownMenu: function (ev) { + ev.stopPropagation(); + }, + /** * Whenever the connection is lost, we need to notify the user. * * @private diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 81211788246..7be17700f98 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -153,10 +153,27 @@ var dom = { * * @param {jQuery} $from - the jQuery element(s) from which to search * @param {string} selector - the CSS selector to match + * @param {boolean} [addBack=false] - whether or not the $from element + * should be considered in the results * @returns {jQuery} */ - cssFind: function ($from, selector) { - return $from.find('*').filter(selector); + cssFind: function ($from, selector, addBack) { + var $results; + + // No way to correctly parse a complex jQuery selector but having no + // spaces should be a good-enough condition to use a simple find + var multiParts = selector.indexOf(' ') >= 0; + if (multiParts) { + $results = $from.find('*').filter(selector); + } else { + $results = $from.find(selector); + } + + if (addBack && $from.is(selector)) { + $results = $results.add($from); + } + + return $results; }, /** * Detaches widgets from the DOM and performs their on_detach_callback() diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 2330307b7a9..713ade2a989 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -808,7 +808,7 @@ var FieldBoolean = AbstractField.extend({ * @returns {jQuery} the focusable checkbox input */ getFocusableElement: function () { - return this.$input || $(); + return this.mode === 'readonly' ? $() : this.$input; }, /** * A boolean field is always set since false is a valid value. @@ -1042,7 +1042,13 @@ var FieldFloatToggle = AbstractField.extend({ * @returns {float} The current formatted value */ _getDisplayedValue: function () { - return parseFloat(this._formatValue(this.value)); + // this.value is a plain float + // Matches what is in Database + var usrFormatValue = this._formatValue(this.value); + // usrFormatValue is string + // contains a float represented in a user specific format + // the float is the fraction by [this.factor] of this.value + return field_utils.parse['float'](usrFormatValue); }, /** * Formats the HTML input tag for edit mode and stores selection status. @@ -1069,7 +1075,7 @@ var FieldFloatToggle = AbstractField.extend({ * one is not in the range, the next value of the closest one will be chosen. * * @private - * @returns {number} The next formatted value in the range + * @returns {number} The next value in the range */ _nextValue: function () { var range = this.nodeOptions.range; @@ -1098,7 +1104,8 @@ var FieldFloatToggle = AbstractField.extend({ if (this.mode === 'edit') { ev.stopPropagation(); // only stop propagation in edit mode var next_val = this._nextValue(); - this._setValue(next_val.toString()); // will be parsed in _setValue + next_val = field_utils.format['float'](next_val); + this._setValue(next_val); // will be parsed in _setValue } }, diff --git a/addons/web/static/src/js/libs/bootstrap.js b/addons/web/static/src/js/libs/bootstrap.js index 9db99815009..b91728b22d1 100644 --- a/addons/web/static/src/js/libs/bootstrap.js +++ b/addons/web/static/src/js/libs/bootstrap.js @@ -6,7 +6,7 @@ odoo.define('web.bootstrap.extensions', function () { * patching in place. */ -/* Bootstrap defaults overwrite */ +/* Bootstrap tooltip defaults overwrite */ $.fn.tooltip.Constructor.Default.placement = 'auto'; $.fn.tooltip.Constructor.Default.fallbackPlacement = ['bottom', 'right', 'left', 'top']; $.fn.tooltip.Constructor.Default.html = true; @@ -24,4 +24,8 @@ $.fn.tooltip.Constructor.prototype.show = function () { return bootstrapShowFunction.call(this); }; + +/* Bootstrap popover defaults overwrite */ +$.fn.popover.Constructor.Default.sanitize = false; + }); diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js index 2c91c795df3..31b39e97abb 100644 --- a/addons/web/static/src/js/public/public_root.js +++ b/addons/web/static/src/js/public/public_root.js @@ -2,6 +2,7 @@ odoo.define('web.public.root', function (require) { 'use strict'; var ajax = require('web.ajax'); +var dom = require('web.dom'); var ServiceProviderMixin = require('web.ServiceProviderMixin'); var session = require('web.session'); var utils = require('web.utils'); @@ -199,7 +200,7 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, { var defs = _.map(this._getPublicWidgetsRegistry(options), function (PublicWidget) { var selector = PublicWidget.prototype.selector || ''; - var $target = $from.find(selector).addBack(selector); + var $target = dom.cssFind($from, selector, true); var defs = _.map($target, function (el) { var widget = new PublicWidget(self, options); @@ -293,8 +294,8 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, { var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit'); _.each($buttons, function (btn) { var $btn = $(btn); - $btn.attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text()); - $btn.button('loading'); + $btn.html('<i class="fa fa-spinner fa-spin"></i> ' + $btn.text()); + $btn.prop('disabled', true); }); }, /** diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js index 54e98bbb87e..e5eff8d7fc1 100644 --- a/addons/web/static/src/js/tools/debug_manager.js +++ b/addons/web/static/src/js/tools/debug_manager.js @@ -436,6 +436,7 @@ DebugManager.include({ var fields = state.fields; var fieldsInfo = state.fieldsInfo.form; var fieldNamesInView = state.getFieldNames(); + var fieldNamesOnlyOnView = ['message_attachment_count']; var fieldsValues = state.data; var modifierDatas = {}; _.each(fieldNamesInView, function (fieldName) { @@ -444,6 +445,7 @@ DebugManager.include({ }); }); this.fields = _.chain(fieldNamesInView) + .difference(fieldNamesOnlyOnView) .map(function (fieldName) { var modifierData = modifierDatas[fieldName]; var invisibleOrReadOnly; diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index c7a80864a15..607f49c8031 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -495,7 +495,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { additional_context: _.extend({}, data.context) }); } - return false; }, /** * FIXME: this logic should be rethought diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 6aa884bbacf..c87a6aca455 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -564,9 +564,16 @@ var BasicModel = AbstractModel.extend({ } } else if (field.type === 'one2many' || field.type === 'many2many') { if (options.raw) { - relDataPoint = this.localData[data[fieldName]]; - relDataPoint = this._applyX2ManyOperations(relDataPoint); - data[fieldName] = relDataPoint.res_ids; + if (typeof data[fieldName] === 'string') { + relDataPoint = this.localData[data[fieldName]]; + relDataPoint = this._applyX2ManyOperations(relDataPoint); + data[fieldName] = relDataPoint.res_ids; + } else { + // no datapoint has been created yet (because the loading of relational + // data has been batched, and hasn't started yet), so the value is still + // the list of ids in the relation + data[fieldName] = data[fieldName] || []; + } } else { data[fieldName] = this.get(data[fieldName]) || []; } diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index dda3978635b..61a966972aa 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -204,7 +204,9 @@ var CalendarController = AbstractController.extend({ * @param {OdooEvent} event */ _onDropRecord: function (event) { - this._updateRecord(event.data); + this._updateRecord(_.extend({}, event.data, { + 'drop': true, + })); }, /** * @private diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 1bcf4fbb298..0ad1b61e4d7 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -60,10 +60,11 @@ return AbstractModel.extend({ end.add(-1, 'days'); } + var isDateEvent = this.fields[this.mapping.date_start].type === 'date'; // An "allDay" event without the "all_day" option is not considered // as a 24h day. It's just a part of the day (by default: 7h-19h). if (event.allDay) { - if (!this.mapping.all_day) { + if (!this.mapping.all_day && !isDateEvent) { if (event.r_start) { start.hours(event.r_start.hours()) .minutes(event.r_start.minutes()) @@ -104,7 +105,9 @@ return AbstractModel.extend({ } if (this.mapping.date_delay) { - data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600; + if (this.data.scale !== 'month' || (this.data.scale === 'month' && !event.drop)) { + data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600; + } } return data; diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js index a1da2925eb6..5ce53b20806 100644 --- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js +++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js @@ -18,7 +18,6 @@ var ControlPanelRenderer = Renderer.extend({ get_action_info: '_onGetActionInfo', }, events: _.extend({}, Renderer.prototype.events, { - 'click.bs.dropdown .o_search_options .dropdown-menu': '_onDropdownClicked', 'click .o_searchview_more': '_onMore', }), @@ -368,15 +367,6 @@ var ControlPanelRenderer = Renderer.extend({ //-------------------------------------------------------------------------- /** - * Prevent the search dropdowns from closing when clicking inside them. - * - * @private - * @param {MouseEvent} ev - */ - _onDropdownClicked: function (ev) { - ev.stopPropagation(); - }, - /** * Toggle the search menus visibility. * * @private diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 10d3c0d54c5..ffeef454716 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -466,7 +466,8 @@ var ListController = BasicController.extend({ _update: function () { return this._super.apply(this, arguments) .then(this._toggleSidebar.bind(this)) - .then(this._toggleCreateButton.bind(this)); + .then(this._toggleCreateButton.bind(this)) + .then(this._updateButtons.bind(this, 'readonly')); }, /** * This helper simply makes sure that the control panel buttons matches the diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index c68f5ab573a..aaf073eeea0 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -162,7 +162,7 @@ ListRenderer.include({ * This is a specialized version of confirmChange, meant to be called when * the change may have affected more than one line (so, for example, an * onchange which add/remove a few lines in a x2many. This does not occur - * in a normal list view) + * in a normal list view). * * The update is more difficult when other rows could have been changed. We * need to potentially remove some lines, add some other lines, update some @@ -172,6 +172,9 @@ ListRenderer.include({ * except the 'main' one (the row which caused the update), then rerender * every new row and add them before/after the main one. * + * Note that this function assumes that the list isn't grouped, which is + * fine as it's never the case for x2many lists. + * * @param {Object} state * @param {string} id * @param {string[]} fields @@ -226,18 +229,17 @@ ListRenderer.include({ } } - // remove all rows except the one being edited, and insert rows - // of the re-rendered body before and after it + // remove all data rows except the one being edited, and insert + // data rows of the re-rendered body before and after it var $editedRow = self._getRow(id); - $editedRow.nextAll().remove(); - $editedRow.prevAll().remove(); + $editedRow.nextAll('.o_data_row').remove(); + $editedRow.prevAll('.o_data_row').remove(); var $newRow = $newBody.find('.o_data_row[data-id="' + id + '"]'); - var $tbody = self.$('tbody'); - $newRow.prevAll().each(function (i, prevRow) { - $tbody.prepend($(prevRow)); + $newRow.prevAll('.o_data_row').get().reverse().forEach(function (row) { + $(row).insertBefore($editedRow); }); - $newRow.nextAll().each(function (i, nextRow) { - $tbody.append($(nextRow)); + $newRow.nextAll('.o_data_row').get().reverse().forEach(function (row) { + $(row).insertAfter($editedRow); }); if (self.currentRow !== null) { @@ -309,7 +311,16 @@ ListRenderer.include({ if (this.state.count >= 4) { $row.remove(); } else { - $row.replaceWith(this._renderEmptyRow()); + // we want to always keep at least 4 (possibly empty) rows + var $emptyRow = this._renderEmptyRow(); + $row.replaceWith($emptyRow); + if (this.editable === "top") { + // move the empty row we just inserted after data rows + var $lastDataRow = this.$('.o_data_row:last'); + if ($lastDataRow.length) { + $emptyRow.insertAfter($lastDataRow); + } + } } }, /** diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 4bf0ac82941..0d04fd69585 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -441,7 +441,6 @@ var ListRenderer = BasicRenderer.extend({ * @returns {jQuery} a <button> element */ _renderGroupButton: function (list, node) { - var self = this; var $button = viewUtils.renderButtonFromNode(node, { extraClass: node.attrs.icon ? 'o_icon_button' : undefined, textAsTitle: !!node.attrs.icon, @@ -449,20 +448,10 @@ var ListRenderer = BasicRenderer.extend({ this._handleAttributes($button, node); this._registerModifiers(node, list.groupData, $button); - // TODO this should be moved to a handler - $button.on("click", function (e) { - e.stopPropagation(); - if (node.attrs.type === 'edit') { - self.trigger_up('group_edit_button_clicked', { - record: list.groupData, - }); - } else { - self.trigger_up('button_clicked', { - attrs: node.attrs, - record: list.groupData, - }); - } - }); + // TODO this should be moved to event handlers + $button.on("click", this._onGroupButtonClicked.bind(this, list.groupData, node)); + $button.on("keydown", this._onGroupButtonKeydown.bind(this)); + return $button; }, /** @@ -889,6 +878,40 @@ var ListRenderer = BasicRenderer.extend({ //-------------------------------------------------------------------------- /** + * @private + * @param {Object} record a record dataPoint on which the button applies + * @param {Object} node arch node of the button + * @param {Object} node.attrs the attrs of the button in the arch + * @param {jQueryEvent} ev + */ + _onGroupButtonClicked: function (record, node, ev) { + ev.stopPropagation(); + if (node.attrs.type === 'edit') { + this.trigger_up('group_edit_button_clicked', { + record: record, + }); + } else { + this.trigger_up('button_clicked', { + attrs: node.attrs, + record: record, + }); + } + }, + /** + * If the user presses ENTER on a group header button, we want to execute + * the button action. This is done automatically as the click handler is + * called. However, we have to stop the propagation of the event to prevent + * another handler from closing the group (see _onKeyDown). + * + * @private + * @param {jQueryEvent} ev + */ + _onGroupButtonKeydown: function (ev) { + if (ev.keyCode === $.ui.keyCode.ENTER) { + ev.stopPropagation(); + } + }, + /** * Manages the keyboard events on the list. If the list is not editable, when the user navigates to * a cell using the keyboard, if he presses enter, enter the model represented by the line * @@ -943,8 +966,11 @@ var ListRenderer = BasicRenderer.extend({ break; } if ($futureCell) { - // If the cell contains activable elements, focus them instead - var $activables = $futureCell.find(':focusable'); + // If the cell contains activable elements, focus them instead (except if it is in a + // group header, in which case we want to activate the whole header, so that we can + // open/close it with RIGHT/LEFT keystrokes) + var isInGroupHeader = $futureCell.closest('tr').hasClass('o_group_header'); + var $activables = !isInGroupHeader && $futureCell.find(':focusable'); if ($activables.length) { $activables[0].focus(); } else { diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 4ae0e1833b6..3206ef7b47a 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -14,7 +14,9 @@ var DateWidget = Widget.extend({ events: { 'change.datetimepicker': 'changeDatetime', 'change .o_datepicker_input': 'changeDatetime', + 'click input': '_onInputClicked', 'input input': '_onInput', + 'keydown': '_onKeydown', 'show.datetimepicker': '_onDateTimePickerShow', 'hide.datetimepicker': '_onDateTimePickerHide', }, @@ -50,10 +52,13 @@ var DateWidget = Widget.extend({ }, widgetParent: 'body', keyBinds: null, - allowInputToggle: true, }, options || {}); this.__libInput = 0; + // tempusdominus doesn't offer any elegant way to check whether the + // datepicker is open or not, so we have to listen to hide/show events + // and manually keep track of the 'open' state + this.__isOpen = false; }, /** * @override @@ -238,6 +243,7 @@ var DateWidget = Widget.extend({ * @private */ _onDateTimePickerHide: function () { + this.__isOpen = false; if (this._onScroll) { window.removeEventListener('scroll', this._onScroll, true); } @@ -250,6 +256,7 @@ var DateWidget = Widget.extend({ * @private */ _onDateTimePickerShow: function () { + this.__isOpen = true; if (this.$input.val().length !== 0 && this.isValid()) { this.$input.select(); } @@ -264,6 +271,34 @@ var DateWidget = Widget.extend({ window.addEventListener('scroll', this._onScroll, true); }, /** + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ESCAPE) { + this.__libInput++; + this.$el.datetimepicker('hide'); + this.__libInput--; + } + }, + /** + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ESCAPE) { + if (this.__isOpen) { + // we don't want any other effects than closing the datepicker, + // like leaving the edition of a row in editable list view + ev.stopImmediatePropagation(); + this.__libInput++; + this.$el.datetimepicker('hide'); + this.__libInput--; + this.focus(); + } + } + }, + /** * Prevents 'input' events triggered by the library to bubble up, as they * might have unwanted effects (like triggering 'field_changed' events in * the context of field widgets) @@ -276,6 +311,15 @@ var DateWidget = Widget.extend({ ev.stopImmediatePropagation(); } }, + /** + * @private + */ + _onInputClicked: function () { + this.__libInput++; + this.$el.datetimepicker('toggle'); + this.__libInput--; + this.focus(); + }, }); var DateTimeWidget = DateWidget.extend({ |
