[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:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user