[IMP] web_tour: tip location

Choosing where to place a tip is a difficult problem.

If placed in the body element, the tip have to be repositionned on each
scroll event which is performance-consuming and induces bugs (firefox
scroll event, tips which is over an element while the anchor is under,
...).
If placed in the anchor parent which scrolls, all the above problems
are solved but when the tip opens it cannot overflow its parent
boundaries, which is not possible in many contexts (modals, ...).

The solution here is to change the tip location when it opens/closes.
When closed, it is placed in the anchor parent which scrolls. This
allows to not have to reposition the tip on each scroll. The tip is
also naturally hidden when its anchor is not visible on the screen.
When opened, the tip is placed in the body, where it can be placed
on the top of anything in the DOM.

The transition from one state to another is not noticable as the
position does not have to be computed when the tip is moved in the
DOM and the animation is purely CSS.

Note: however, this commit does not solve the firefox bug of closing
the tip when the window scrolls. Here, the temporary solution is to
let the tip be wrongly position if the user scrolls when a tip is
opened (this only occurs when using the mouse wheel anyway).
This commit is contained in:
qsm-odoo
2016-06-24 17:18:44 +02:00
parent 25a3a6f9bf
commit b70c990b6a
3 changed files with 71 additions and 8 deletions
+65 -6
View File
@@ -11,7 +11,6 @@ return Widget.extend({
mouseleave: "_to_bubble_mode",
},
/**
* @param {$anchor} [JQuery] the node on which the tip should be placed
* @param {info} [Object] description of the tip, containing the following keys:
* - content [String] the html content of the tip
* - event_handlers [Object] description of optional event handlers to bind to the tip:
@@ -24,9 +23,8 @@ return Widget.extend({
* - overlay [Object] x and y values for the number of pixels the mouseout detection area
* overlaps the opened tip, default {x: 50, y: 50}
*/
init: function(parent, $anchor, info) {
init: function(parent, info) {
this._super(parent);
this.$anchor = $anchor;
this.info = _.defaults(info, {
position: "right",
width: 270,
@@ -36,6 +34,24 @@ return Widget.extend({
y: 50,
},
});
this.position = {
top: "50%",
left: "50%",
};
},
/**
* @param {$anchor} [JQuery] the node on which the tip should be placed
*/
attach_to: function ($anchor) {
this.$anchor = $anchor;
this.$ideal_location = this._get_ideal_location();
var position = this.$ideal_location.css("position");
if (position === "static") {
this.$ideal_location.addClass("o_tooltip_parent");
}
return this.appendTo(this.$ideal_location);
},
start: function() {
this.$tooltip_overlay = this.$(".o_tooltip_overlay");
@@ -59,30 +75,58 @@ return Widget.extend({
this._reposition();
this.$el.css("opacity", 1);
core.bus.on('scroll resize', this, function() {
core.bus.on("resize", this, function () {
if (this.tip_opened) {
this._to_bubble_mode(true);
}
this._reposition();
});
this.$el.on("transitionend oTransitionEnd webkitTransitionEnd", (function () {
if (!this.tip_opened && this.$el.parent()[0] === document.body) {
this.$el.detach();
this.$el.css(this.position);
this.$el.appendTo(this.$ideal_location);
}
}).bind(this));
return this._super.apply(this, arguments);
},
destroy: function () {
this._unbind_anchor_events();
clearTimeout(this.timerIn);
clearTimeout(this.timerOut);
// Do not remove the parent class if it contains other tooltips
if (this.$ideal_location.children(".o_tooltip").not(this.$el[0]).length === 0) {
this.$ideal_location.removeClass("o_tooltip_parent");
}
return this._super.apply(this, arguments);
},
update: function($anchor) {
update: function ($anchor) {
if (!$anchor.is(this.$anchor)) {
this._unbind_anchor_events();
this.$anchor = $anchor;
this.$ideal_location = this._get_ideal_location();
if (this.$el.parent()[0] !== document.body) {
this.$el.appendTo(this.$ideal_location);
}
this._bind_anchor_events();
}
this._reposition();
},
_reposition: function() {
_get_ideal_location: function () {
var $location = this.$anchor;
var o;
do {
$location = $location.parent();
o = $location.css("overflow");
} while ((o === "visible" || o === "hidden") && $location[0] !== document.body);
return $location;
},
_reposition: function () {
if (this.tip_opened) return;
this.$el.removeClass("o_animated");
@@ -101,6 +145,8 @@ return Widget.extend({
left: -Math.min((this.info.position === "right" ? this.info.space : this.info.overlay.x), offset.left),
});
this.position = this.$el.position();
this.$el.addClass("o_animated");
},
_bind_anchor_events: function () {
@@ -135,6 +181,9 @@ return Widget.extend({
this.timerOut = undefined;
return;
}
if (this.tip_opened) {
return;
}
if (force === true) {
this._build_info_mode();
@@ -149,6 +198,13 @@ return Widget.extend({
this.tip_opened = true;
var offset = this.$el.offset();
if (this.$el.parent()[0] !== document.body) {
this.$el.detach();
this.$el.css(offset);
this.$el.appendTo(document.body);
}
var mbLeft = 0;
var mbTop = 0;
var overflow = false;
@@ -180,6 +236,9 @@ return Widget.extend({
this.timerIn = undefined;
return;
}
if (!this.tip_opened) {
return;
}
if (force === true) {
this._build_bubble_mode();
@@ -125,9 +125,9 @@ return core.Class.extend({
}],
});
}
tip.widget = new Tip(this, $anchor, tip_info);
tip.widget.appendTo(document.body);
tip.widget = new Tip(this, tip_info);
tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));
tip.widget.attach_to($anchor);
if (this.running_tour === tour_name) {
clearTimeout(this.running_tour_timeout);
+4
View File
@@ -1,4 +1,8 @@
.o_tooltip_parent {
position: relative!important;
}
.o_tooltip {
@o-tip-background-color: lighten(@odoo-brand-primary, 5%);
@o-tip-size: 26px;