[IMP] web_tour: add alternative selector to consume step

* Prior to this commit only one selector could be provided in order to got to the
  next tour step.

* After this commit an additional selector can be provided in order to consume
  a tour step. This additional selector can be specified through the step property
  alt_trigger. We also toke the opportunity to remove $actualAnchor in tip.js as no
  more used.

task-2325001
Related PR: odoo/enterprise#12711

closes odoo/odoo#57680

X-original-commit: 9606e9e231be9e1e7d15ea0efdedbf5afff47102
Related: odoo/enterprise#13214
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Laurent Stukkens (LTU)
2020-09-14 15:53:45 +00:00
parent c4442e8341
commit 2d930cbccf
2 changed files with 94 additions and 35 deletions
+68 -31
View File
@@ -54,10 +54,17 @@ var Tip = Widget.extend({
this._onAncestorScroll = _.throttle(this._onAncestorScroll, 50);
},
/**
* Attaches the tip to the provided $anchor and $altAnchor.
* $altAnchor is an alternative trigger that can consume the step. The tip is
* however only displayed on the $anchor.
*
* @param {jQuery} $anchor the node on which the tip should be placed
* @param {jQuery} $altAnchor an alternative node that can consume the step
* @return {Promise}
*/
attach_to: async function ($anchor) {
this._setupAnchor($anchor);
attach_to: function ($anchor, $altAnchor) {
this._setupAnchor($anchor, $altAnchor);
this.is_anchor_fixed_position = this.$anchor.css("position") === "fixed";
if (this.info.optional === "true") {
@@ -128,14 +135,22 @@ var Tip = Widget.extend({
return this._super.apply(this, arguments);
},
update: function ($anchor) {
/**
* Updates the $anchor and $altAnchor the tip is attached to.
* $altAnchor is an alternative trigger that can consume the step. The tip is
* however only displayed on the $anchor.
*
* @param {jQuery} $anchor the node on which the tip should be placed
* @param {jQuery} $altAnchor an alternative node that can consume the step
*/
update: function ($anchor, $altAnchor) {
// We unbind/rebind events on each update because we support widgets
// detaching and re-attaching nodes to their DOM element without keeping
// the initial event handlers, with said node being potential tip
// anchors (e.g. FieldMonetary > input element).
this._unbind_anchor_events();
if (!$anchor.is(this.$anchor)) {
this._setupAnchor($anchor);
this._setupAnchor($anchor, $altAnchor);
}
this._bind_anchor_events();
if (!this.$el) {
@@ -152,12 +167,16 @@ var Tip = Widget.extend({
//--------------------------------------------------------------------------
/**
* @private
* @param {jQuery} $anchor
* Sets the $anchor and $altAnchor the tip is attached to.
* $altAnchor is an alternative trigger that can consume the step. The tip is
* however only displayed on the $anchor.
*
* @param {jQuery} $anchor the node on which the tip should be placed
* @param {jQuery} $altAnchor an alternative node that can consume the step
*/
_setupAnchor: function ($anchor) {
_setupAnchor: function ($anchor, $altAnchor) {
this.$anchor = $anchor;
this.$actualAnchor = this.$anchor;
this.$altAnchor = $altAnchor;
this.$ideal_location = this._get_ideal_location();
this.$furtherIdealLocation = this._get_ideal_location(this.$ideal_location);
},
@@ -214,11 +233,9 @@ var Tip = Widget.extend({
let $location;
if (this.viewPortState === 'in') {
this.$tooltip_content.html(this.info.content);
this.$actualAnchor = this.$anchor;
$location = this.$ideal_location;
} else {
this.$tooltip_content.html(this.info.scrollContent);
this.$actualAnchor = this.$ideal_location;
$location = this.$furtherIdealLocation;
}
// Update o_tooltip_parent class and tip DOM location. Note:
@@ -320,37 +337,57 @@ var Tip = Widget.extend({
this.$el.addClass("o_animated");
},
_bind_anchor_events: function () {
// The consume_event taken for RunningTourActionHelper is the one of $anchor and not $altAnchor.
this.consume_event = this.info.consumeEvent || Tip.getConsumeEventType(this.$anchor, this.info.run);
this.$consumeEventAnchor = this.$anchor;
if (this.consume_event === "drag") {
// jQuery-ui draggable triggers 'drag' events on the .ui-draggable element,
// but the tip is attached to the .ui-draggable-handle element which may
// be one of its children (or the element itself)
this.$consumeEventAnchor = this.$anchor.closest('.ui-draggable');
} else if (this.consume_event === "input" && !this.$anchor.is('textarea, input')) {
this.$consumeEventAnchor = this.$anchor.closest("[contenteditable='true']");
} else if (this.consume_event.includes('apply.daterangepicker')) {
this.$consumeEventAnchor = this.$anchor.parent().children('.o_field_date_range');
} else if (this.consume_event === "sort") {
// when an element is dragged inside a sortable container (with classname
// 'ui-sortable'), jQuery triggers the 'sort' event on the container
this.$consumeEventAnchor = this.$anchor.closest('.ui-sortable');
this.$consumeEventAnchors = this._getAnchorAndCreateEvent(this.consume_event, this.$anchor);
if (this.$altAnchor.length) {
const consumeEvent = this.info.consumeEvent || Tip.getConsumeEventType(this.$altAnchor, this.info.run);
this.$consumeEventAnchors = this.$consumeEventAnchors.add(
this._getAnchorAndCreateEvent(consumeEvent, this.$altAnchor)
);
}
this.$consumeEventAnchor.on(this.consume_event + ".anchor", (function (e) {
if (e.type !== "mousedown" || e.which === 1) { // only left click
this.trigger("tip_consumed");
this._unbind_anchor_events();
}
}).bind(this));
this.$anchor.on('mouseenter.anchor', () => this._to_info_mode());
this.$anchor.on('mouseleave.anchor', () => this._to_bubble_mode());
this.$scrolableElement = this.$ideal_location.is('html,body') ? $(window) : this.$ideal_location;
this.$scrolableElement.on('scroll.Tip', () => this._onAncestorScroll());
},
/**
* Gets the anchor corresponding to the provided arguments and attaches the
* event to the $anchor in order to consume the step accordingly.
*
* @private
* @param {String} consumeEvent
* @param {jQuery} $anchor the node on which the tip should be placed
* @return {jQuery}
*/
_getAnchorAndCreateEvent: function(consumeEvent, $anchor) {
let $consumeEventAnchors = $anchor;
if (consumeEvent === "drag") {
// jQuery-ui draggable triggers 'drag' events on the .ui-draggable element,
// but the tip is attached to the .ui-draggable-handle element which may
// be one of its children (or the element itself)
$consumeEventAnchors = $anchor.closest('.ui-draggable');
} else if (consumeEvent === "input" && !$anchor.is('textarea, input')) {
$consumeEventAnchors = $anchor.closest("[contenteditable='true']");
} else if (consumeEvent.includes('apply.daterangepicker')) {
$consumeEventAnchors = $anchor.parent().children('.o_field_date_range');
} else if (consumeEvent === "sort") {
// when an element is dragged inside a sortable container (with classname
// 'ui-sortable'), jQuery triggers the 'sort' event on the container
$consumeEventAnchors = $anchor.closest('.ui-sortable');
}
$consumeEventAnchors.on(consumeEvent + ".anchor", (function (e) {
if (e.type !== "mousedown" || e.which === 1) { // only left click
this.trigger("tip_consumed");
this._unbind_anchor_events();
}
}).bind(this));
return $consumeEventAnchors;
},
_unbind_anchor_events: function () {
this.$anchor.off(".anchor");
this.$consumeEventAnchor.off(".anchor");
this.$consumeEventAnchors.off(".anchor");
this.$scrolableElement.off('.Tip');
},
_get_spaced_inverted_position: function (position) {
+26 -4
View File
@@ -283,12 +283,23 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
extra_trigger = get_first_visible_element($extra_trigger).length;
}
var $visible_alt_trigger = $();
if (tip.alt_trigger) {
var $alt_trigger;
if (tip.in_modal !== false && this.$modal_displayed.length) {
$alt_trigger = this.$modal_displayed.find(tip.alt_trigger);
} else {
$alt_trigger = get_jquery_element_from_selector(tip.alt_trigger);
}
$visible_alt_trigger = get_first_visible_element($alt_trigger);
}
var triggered = $visible_trigger.length && extra_trigger;
if (triggered) {
if (!tip.widget) {
this._activate_tip(tip, tour_name, $visible_trigger);
this._activate_tip(tip, tour_name, $visible_trigger, $visible_alt_trigger);
} else {
tip.widget.update($visible_trigger);
tip.widget.update($visible_trigger, $visible_alt_trigger);
}
} else {
if ($trigger.iframeContainer || ($extra_trigger && $extra_trigger.iframeContainer)) {
@@ -322,7 +333,18 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
}
return !!triggered;
},
_activate_tip: function(tip, tour_name, $anchor) {
/**
* Activates the provided tip for the provided tour, $anchor and $alt_trigger.
* $alt_trigger is an alternative trigger that can consume the step. The tip is
* however only displayed on the $anchor.
*
* @param {Object} tip
* @param {String} tour_name
* @param {jQuery} $anchor
* @param {jQuery} $alt_trigger
* @private
*/
_activate_tip: function(tip, tour_name, $anchor, $alt_trigger) {
var tour = this.tours[tour_name];
var tip_info = tip;
if (tour.skip_link) {
@@ -339,7 +361,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
if (this.running_tour !== tour_name) {
tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));
}
tip.widget.attach_to($anchor).then(this._to_next_running_step.bind(this, tip, tour_name));
tip.widget.attach_to($anchor, $alt_trigger).then(this._to_next_running_step.bind(this, tip, tour_name));
},
_deactivate_tip: function(tip) {
if (tip && tip.widget) {