[IMP] web: trigger an event when webclient is ready

The main purpose of this commit is to be consistent with the enterprise edition,
in which the event 'DOM_updated' is triggered when new content is inserted in
the DOM. This way, addons that need to know when a view has been attached in the DOM
(e.g. web_tip) can listen to this event and will work fine with both editions
of the web addon.

With the actual structure of the webclient (action_manager, view_manager),
there is no easy way to decide whether or not a view is attached in the DOM,
because every rendering is performed in a detached element. For instance,
when a view_manager instantiates a new view, it appends it in its $el once
the view is ready, but it can't know if its $el is attached in the DOM (it is
only the case if the view_manager is the current action of the action manager).

This rev. introduces utility functions to centralize the appending/prepending of
content, so that an event can be triggered (at a unique place) when the content
is really in the DOM. These functions are used by the the action_manager and the
view_manager.

Another change is that the action_manager keeps the view_manager informed when
they are attached in/detached from the DOM, through the variable is_in_DOM.
This commit is contained in:
Aaron Bohy
2015-07-15 13:32:58 +02:00
parent 27df65c878
commit a0ddaf8b84
5 changed files with 119 additions and 29 deletions
+49 -5
View File
@@ -43,6 +43,18 @@ var Action = core.Class.extend({
set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) { set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) {
this.on_reverse_breadcrumb = 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 * @return {Object} the description of the action
*/ */
@@ -61,6 +73,12 @@ var Action = core.Class.extend({
get_nb_views: function() { get_nb_views: function() {
return 0; 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 * Specialization of Action for client actions that are Widgets
@@ -108,6 +126,13 @@ var ViewManagerAction = WidgetAction.extend({
_super.call(self); _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 * @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 // document only when it's ready
var new_widget_fragment = document.createDocumentFragment(); var new_widget_fragment = document.createDocumentFragment();
return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() { 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 // Hide the old_widget as it will be removed from the DOM when it
// is destroyed // is destroyed
if (old_widget) { if (old_widget) {
@@ -269,17 +302,28 @@ var ActionManager = Widget.extend({
select_action: function(action, index) { select_action: function(action, index) {
var self = this; var self = this;
return this.webclient.clear_uncommitted_changes().then(function() { return this.webclient.clear_uncommitted_changes().then(function() {
// Set the new inner_widget and clear the action_stack // Set the new inner_action/widget and update the action stack
self.inner_widget = action.widget; var old_action = self.inner_action;
var action_index = self.action_stack.indexOf(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 // Hide the ControlPanel if the widget doesn't use it
if (!self.inner_widget.need_control_panel) { if (!self.inner_widget.need_control_panel) {
self.main_control_panel.do_hide(); 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() { }).fail(function() {
return $.Deferred().reject(); return $.Deferred().reject();
}); });
@@ -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 { return {
blockUI: blockUI, blockUI: blockUI,
unblockUI: unblockUI, unblockUI: unblockUI,
redirect: redirect, redirect: redirect,
append: append,
prepend: prepend,
}; };
}); });
+1 -10
View File
@@ -166,18 +166,9 @@ var View = Widget.extend({
this.embedded_view = embedded_view; this.embedded_view = embedded_view;
}, },
do_show: function () { do_show: function () {
var self = this; this._super();
this.$el.show();
setTimeout(function () {
self.$el.parent().addClass('in');
}, 0);
core.bus.trigger('view_shown', this); core.bus.trigger('view_shown', this);
}, },
do_hide: function () {
this.$el.parent().removeClass('in');
this.$el.hide();
},
is_active: function () { is_active: function () {
return this.ViewManager.active_view.controller === this; return this.ViewManager.active_view.controller === this;
}, },
+31 -13
View File
@@ -4,6 +4,7 @@ odoo.define('web.ViewManager', function (require) {
var ControlPanelMixin = require('web.ControlPanelMixin'); var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core'); var core = require('web.core');
var data = require('web.data'); var data = require('web.data');
var framework = require('web.framework');
var Model = require('web.DataModel'); var Model = require('web.DataModel');
var pyeval = require('web.pyeval'); var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView'); var SearchView = require('web.SearchView');
@@ -45,6 +46,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.active_view = null; this.active_view = null;
this.registry = core.view_registry; this.registry = core.view_registry;
this.title = this.action && this.action.name; 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) { _.each(views, function (view) {
var view_type = view[1] || view.view_type; var view_type = view[1] || view.view_type;
var View = core.view_registry.get(view_type, true); 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) { switch_mode: function(view_type, no_store, view_options) {
var self = this; var self = this;
var view = this.views[view_type]; var view = this.views[view_type];
var old_view = this.active_view;
var switched = $.Deferred();
if (!view) { if (!view) {
return $.Deferred().reject(); return $.Deferred().reject();
@@ -153,19 +157,23 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
self.searchview.do_search(); self.searchview.do_search();
}); });
} }
return $.when(view.created, this.active_search).done(function () { $.when(view.created, this.active_search).done(function () {
self._display_view(view_options); self._display_view(view_options, old_view).done(function() {
self.trigger('switch_mode', view_type, no_store, view_options); 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 self = this;
var view_controller = this.active_view.controller; var view_controller = this.active_view.controller;
var view_fragment = this.active_view.$fragment;
var view_control_elements = this.render_view_control_elements(); var view_control_elements = this.render_view_control_elements();
// Show the view // Show the view
this.active_view.$container.show(); 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 // Prepare the ControlPanel content and update it
var cp_status = { var cp_status = {
active_view_selector: '.oe-cp-switch-' + self.active_view.type, 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, search_view_hidden: view_controller.searchable === false,
}; };
self.update_control_panel(cp_status); 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) { create_view: function(view, view_options) {
@@ -189,11 +206,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
options.initial_mode = options.initial_mode || 'edit'; options.initial_mode = options.initial_mode || 'edit';
} }
var controller = new View(this, this.dataset, view.view_id, options); var controller = new View(this, this.dataset, view.view_id, options);
var $container = view.$container;
$container.hide();
view.controller = controller; view.controller = controller;
view.$container = $container; view.$fragment = $('<div>');
if (view.embedded_view) { if (view.embedded_view) {
controller.set_embedded_view(view.embedded_view); controller.set_embedded_view(view.embedded_view);
@@ -211,10 +225,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
controller.on('view_loaded', this, function () { controller.on('view_loaded', this, function () {
view_loaded.resolve(); view_loaded.resolve();
}); });
return $.when(controller.appendTo($container), view_loaded)
.done(function () { // render the view in a fragment so that it is appended in the view's
self.trigger("controller_inited", view.type, controller); // $container only when it's ready
}); return $.when(controller.appendTo(view.$fragment), view_loaded).done(function () {
// Remove the unnecessary outer div
view.$fragment = view.$fragment.contents();
self.trigger("controller_inited", view.type, controller);
});
}, },
select_view: function (index) { select_view: function (index) {
var view_type = this.view_stack[index].type; var view_type = this.view_stack[index].type;
+1 -1
View File
@@ -480,7 +480,7 @@
<div t-name="ViewManager" class="oe-view-manager"> <div t-name="ViewManager" class="oe-view-manager">
<div class="oe-view-manager-content"> <div class="oe-view-manager-content">
<t t-foreach="widget.views" t-as="view"> <t t-foreach="widget.views" t-as="view">
<div t-attf-class="oe-view-manager-view-#{view} fade"/> <div t-attf-class="oe-view-manager-view-#{view}"/>
</t> </t>
</div> </div>
</div> </div>