[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) {
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();
});
@@ -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,
};
});
+1 -10
View File
@@ -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;
},
+31 -13
View File
@@ -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 = $('<div>');
if (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 () {
view_loaded.resolve();
});
return $.when(controller.appendTo($container), view_loaded)
.done(function () {
self.trigger("controller_inited", view.type, controller);
});
// render the view in a fragment so that it is appended in the view's
// $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) {
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 class="oe-view-manager-content">
<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>
</div>
</div>