diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js index 9b4fd97753c..e3d2b589f27 100644 --- a/addons/web/static/src/js/action_manager.js +++ b/addons/web/static/src/js/action_manager.js @@ -43,6 +43,18 @@ var Action = core.Class.extend({ set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) { this.on_reverse_breadcrumb = on_reverse_breadcrumb; }, + /** + * Stores the DOM fragment of the action + * @param {jQuery} [fragment] the DOM fragment + */ + set_fragment: function($fragment) { + this.$fragment = $fragment; + }, + /** + * Not implemented for client actions + */ + set_is_in_DOM: function() { + }, /** * @return {Object} the description of the action */ @@ -61,6 +73,12 @@ var Action = core.Class.extend({ get_nb_views: function() { return 0; }, + /** + * @return {jQuery} the DOM fragment of the action + */ + get_fragment: function() { + return this.$fragment; + }, }); /** * Specialization of Action for client actions that are Widgets @@ -108,6 +126,13 @@ var ViewManagerAction = WidgetAction.extend({ _super.call(self); }); }, + /** + * Sets is_in_DOM on this.widget + * @param {Boolean} [is_in_DOM] true iff the widget is attached in the DOM + */ + set_is_in_DOM: function(is_in_DOM) { + this.widget.is_in_DOM = is_in_DOM; + }, /** * @return {Array} array of Objects that will be interpreted to display the breadcrumbs */ @@ -215,7 +240,15 @@ var ActionManager = Widget.extend({ // document only when it's ready var new_widget_fragment = document.createDocumentFragment(); return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() { - self.$el.append(new_widget_fragment); + // Detach the fragment of the previous action and store it within the action + if (old_action) { + old_action.set_fragment(old_widget.$el.detach()); + old_action.set_is_in_DOM(false); + } + + framework.append(self.$el, new_widget_fragment, true); + self.inner_action.set_is_in_DOM(true); + // Hide the old_widget as it will be removed from the DOM when it // is destroyed if (old_widget) { @@ -269,17 +302,28 @@ var ActionManager = Widget.extend({ select_action: function(action, index) { var self = this; return this.webclient.clear_uncommitted_changes().then(function() { - // Set the new inner_widget and clear the action_stack - self.inner_widget = action.widget; + // Set the new inner_action/widget and update the action stack + var old_action = self.inner_action; var action_index = self.action_stack.indexOf(action); - self.clear_action_stack(self.action_stack.splice(action_index + 1)); + var to_destroy = self.action_stack.splice(action_index + 1); + self.inner_action = action; + self.inner_widget = action.widget; // Hide the ControlPanel if the widget doesn't use it if (!self.inner_widget.need_control_panel) { self.main_control_panel.do_hide(); } - return action.restore(index); + return $.when(action.restore(index)).done(function() { + if (action !== old_action) { + // Clear the action stack (this also removes the current action from the DOM) + self.clear_action_stack(to_destroy); + + // Append the fragment of the action to restore to self.$el + framework.append(self.$el, action.get_fragment(), true); + self.inner_action.set_is_in_DOM(true); + } + }); }).fail(function() { return $.Deferred().reject(); }); diff --git a/addons/web/static/src/js/framework/misc.js b/addons/web/static/src/js/framework/misc.js index 7a87c05350b..ef073d1ab0d 100644 --- a/addons/web/static/src/js/framework/misc.js +++ b/addons/web/static/src/js/framework/misc.js @@ -322,10 +322,47 @@ $.extend( proto, { } }); +/** + * Private function that triggers an event on core.bus + * @param {htmlString or Element or Array or jQuery} [content] the content that + * has been attached in the DOM + */ +function _notify (content) { + core.bus.trigger('DOM_updated', content); +} +/** + * Appends content in a jQuery object and optionnally triggers an event + * @param {jQuery} [$target] the node where content will be appended + * @param {htmlString or Element or Array or jQuery} [content] DOM element, + * array of elements, HTML string or jQuery object to append to $target + * @param {jQuery} [trigger] true to trigger an event, false otherwise + */ +function append ($target, content, trigger) { + $target.append(content); + if (trigger) { + _notify(content); + } +} +/** + * Prepends content in a jQuery object and optionnally triggers an event + * @param {jQuery} [$target] the node where content will be prepended + * @param {htmlString or Element or Array or jQuery} [content] DOM element, + * array of elements, HTML string or jQuery object to prepend to $target + * @param {jQuery} [trigger] true to trigger an event, false otherwise + */ +function prepend ($target, content, trigger) { + $target.prepend(content); + if (trigger) { + _notify(content); + } +} + return { blockUI: blockUI, unblockUI: unblockUI, redirect: redirect, + append: append, + prepend: prepend, }; }); diff --git a/addons/web/static/src/js/framework/view.js b/addons/web/static/src/js/framework/view.js index 8eee01d414d..d803b7487df 100644 --- a/addons/web/static/src/js/framework/view.js +++ b/addons/web/static/src/js/framework/view.js @@ -166,18 +166,9 @@ var View = Widget.extend({ this.embedded_view = embedded_view; }, do_show: function () { - var self = this; - this.$el.show(); - setTimeout(function () { - self.$el.parent().addClass('in'); - }, 0); - + this._super(); core.bus.trigger('view_shown', this); }, - do_hide: function () { - this.$el.parent().removeClass('in'); - this.$el.hide(); - }, is_active: function () { return this.ViewManager.active_view.controller === this; }, diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js index 9321b010dd1..2f138a07556 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -4,6 +4,7 @@ odoo.define('web.ViewManager', function (require) { var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var data = require('web.data'); +var framework = require('web.framework'); var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); var SearchView = require('web.SearchView'); @@ -45,6 +46,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.active_view = null; this.registry = core.view_registry; this.title = this.action && this.action.name; + this.is_in_DOM = false; // used to know if the view manager is attached in the DOM _.each(views, function (view) { var view_type = view[1] || view.view_type; var View = core.view_registry.get(view_type, true); @@ -120,6 +122,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, { switch_mode: function(view_type, no_store, view_options) { var self = this; var view = this.views[view_type]; + var old_view = this.active_view; + var switched = $.Deferred(); if (!view) { return $.Deferred().reject(); @@ -153,19 +157,23 @@ var ViewManager = Widget.extend(ControlPanelMixin, { self.searchview.do_search(); }); } - return $.when(view.created, this.active_search).done(function () { - self._display_view(view_options); - self.trigger('switch_mode', view_type, no_store, view_options); + $.when(view.created, this.active_search).done(function () { + self._display_view(view_options, old_view).done(function() { + self.trigger('switch_mode', view_type, no_store, view_options); + switched.resolve(); + }); }); + return switched; }, - _display_view: function (view_options) { + _display_view: function (view_options, old_view) { var self = this; var view_controller = this.active_view.controller; + var view_fragment = this.active_view.$fragment; var view_control_elements = this.render_view_control_elements(); // Show the view this.active_view.$container.show(); - $.when(view_controller.do_show(view_options)).done(function () { + return $.when(view_controller.do_show(view_options)).done(function () { // Prepare the ControlPanel content and update it var cp_status = { active_view_selector: '.oe-cp-switch-' + self.active_view.type, @@ -176,6 +184,15 @@ var ViewManager = Widget.extend(ControlPanelMixin, { search_view_hidden: view_controller.searchable === false, }; self.update_control_panel(cp_status); + + // Detach the old view but not ui-autocomplete elements to let + // jquery-ui garbage-collect them + if (old_view) { + old_view.$container.contents().not('.ui-autocomplete').detach(); + } + + // Append the view fragment to its $container + framework.append(self.active_view.$container, view_fragment, self.is_in_DOM); }); }, create_view: function(view, view_options) { @@ -189,11 +206,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, { options.initial_mode = options.initial_mode || 'edit'; } var controller = new View(this, this.dataset, view.view_id, options); - var $container = view.$container; - - $container.hide(); view.controller = controller; - view.$container = $container; + view.$fragment = $('