diff --git a/addons/web_tour/static/src/js/tip.js b/addons/web_tour/static/src/js/tip.js index ab5eac5033c..56ea24f2981 100644 --- a/addons/web_tour/static/src/js/tip.js +++ b/addons/web_tour/static/src/js/tip.js @@ -1,23 +1,59 @@ odoo.define('web_tour.Tip', function(require) { "use strict"; +var core = require('web.core'); var Widget = require('web.Widget'); return Widget.extend({ template: "Tip", + events: { + mouseenter: "_to_info_mode", + 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: + * - event [String] the event name + * - selector [String] the jQuery selector on which the event should be bound + * - handler [function] the handler + * - position [String] tip's position ('top', 'right', 'left' or 'bottom'), default 'right' + * - width [int] the width in px of the tip when opened, default 270 + * - space [int] space in px between anchor and tip, default 10 + * - 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) { this._super(parent); this.$anchor = $anchor; this.info = _.defaults(info, { - position: 'right', + position: "right", + width: 270, + space: 10, + overlay: { + x: 50, + y: 50, + }, }); - this.consumed = false; }, start: function() { - this.$breathing = this.$('.o_breathing'); - this.$breathing.on('mouseenter', this._to_info_mode.bind(this)); + this.init_width = this.$el.innerWidth(); + this.init_height = this.$el.innerHeight(); + this.$tooltip_overlay = this.$(".o_tooltip_overlay"); + this.$tooltip_content = this.$(".o_tooltip_content"); + _.each(this.info.event_handlers, (function(data) { + this.$tooltip_content.on(data.event, data.selector, data.handler); + }).bind(this)); this._bind_anchor_events(); this._reposition(); + core.bus.on('scroll resize', this, function() { + if (this.tip_opened) { + this._to_bubble_mode(); + } + this._reposition(); + }); + return this._super.apply(this, arguments); }, update: function($anchor) { if (!$anchor.is(this.$anchor)) { @@ -28,50 +64,107 @@ return Widget.extend({ this._reposition(); }, _reposition: function() { - this.$breathing.position({ - my: this._invert_position(this.info.position), + if (this.tip_opened) return; + this.$el.removeClass("o_animated"); + this.$el.position({ + my: this._get_spaced_inverted_position(this.info.position), at: this.info.position, of: this.$anchor, - collision: 'fit', + collision: "none", }); + this.$tooltip_overlay.css({ + top: -(this.info.position === "bottom" ? this.info.space : this.info.overlay.y), + right: -(this.info.position === "left" ? this.info.space : this.info.overlay.x), + bottom: -(this.info.position === "top" ? this.info.space : this.info.overlay.y), + left: -(this.info.position === "right" ? this.info.space : this.info.overlay.x), + }); + this.$el.addClass("o_animated"); }, _bind_anchor_events: function () { - var self = this; + var consume_event = this.$anchor.is('input,textarea') ? 'change' : 'mousedown'; + this.$anchor.one(consume_event, this.trigger.bind(this, 'tip_consumed')); this.$anchor.on('mouseenter', this._to_info_mode.bind(this)); this.$anchor.on('mouseleave', this._to_bubble_mode.bind(this)); - this.$anchor.on(this.$anchor.is('input,textarea') ? 'change' : 'mousedown', function () { - if (self.consumed) return; - self.consumed = true; - self.trigger('tip_consumed'); - }); }, - _invert_position: function (position) { - if (position === "right") return "left"; - if (position === "left") return "right"; - if (position === "bottom") return "top"; - return "bottom"; + _get_spaced_inverted_position: function (position) { + if (position === "right") return "left+" + this.info.space; + if (position === "left") return "right-" + this.info.space; + if (position === "bottom") return "top+" + this.info.space; + return "bottom-" + this.info.space; }, - _to_info_mode: function() { - this.$breathing.fadeOut(300); - this.$popover = this.$popover || this.$anchor.popover({ - content: this.info.content + (this.info.extra_content || ''), - html: true, - animation: false, - container: this.$el, - placement: this.info.position, - }); - this.$popover.popover('show'); - this.$('.popover').on('mouseleave.to_bubble_mode', this._to_bubble_mode.bind(this)); - this.$('.popover').on('click', this.trigger.bind(this, 'popover_clicked')); - }, - _to_bubble_mode: function () { - this.$breathing.fadeIn(300); - if (this.$popover) { - this.$popover.popover('hide'); - this.$('.popover').off('mouseleave.to_bubble_mode'); - this.$('.popover').off('click'); + _to_info_mode: function(force) { + if (this.timerOut !== undefined) { + clearTimeout(this.timerOut); + this.timerOut = undefined; + return; + } + + if (force === true) { + this.__to_info_mode(); + } else { + this.timerIn = setTimeout(this.__to_info_mode.bind(this), 100); } }, -}); + __to_info_mode: function() { + clearTimeout(this.timerIn); + this.timerIn = undefined; + var content_width = this.$tooltip_content.outerWidth(); + var content_height = this.$tooltip_content.outerHeight(); + + this.tip_opened = true; + + var offset = this.$el.offset(); + var mbLeft = 0; + var mbTop = 0; + var overflow = false; + var posVertical = (this.info.position === "top" || this.info.position === "bottom"); + if (posVertical) { + overflow = (offset.left + content_width + this.info.overlay.x > $(window).width()); + } else { + overflow = (offset.top + content_height + this.info.overlay.y > $(window).height()); + } + if (posVertical && overflow || this.info.position === "left") { + mbLeft -= (content_width - this.init_width); + } + if (!posVertical && overflow || this.info.position === "top") { + mbTop -= (content_height - this.init_height); + } + + this.$el.toggleClass("inverse", overflow); + this.$el.removeClass("o_animated").addClass("active"); + this.$el.css({ + width: content_width, + height: content_height, + "margin-left": mbLeft, + "margin-top": mbTop, + }); + }, + _to_bubble_mode: function (force) { + if (this.timerIn !== undefined) { + clearTimeout(this.timerIn); + this.timerIn = undefined; + return; + } + + if (force === true) { + this.__to_bubble_mode(); + } else { + this.timerOut = setTimeout(this.__to_bubble_mode.bind(this), 300); + } + }, + __to_bubble_mode: function() { + clearTimeout(this.timerOut); + this.timerOut = undefined; + + this.tip_opened = false; + + this.$el.removeClass("active").addClass("o_animated"); + this.$el.css({ + width: this.init_width, + height: this.init_height, + margin: 0, + }); + }, +}); }); diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js index 9150b499ee6..7e1a4c54006 100644 --- a/addons/web_tour/static/src/js/tour_manager.js +++ b/addons/web_tour/static/src/js/tour_manager.js @@ -19,6 +19,7 @@ function getRunningKey() { return core.Class.extend({ init: function(consumed_tours) { + this.$body = $('body'); this.active_tooltips = {}; this.tours = {}; this.consumed_tours = consumed_tours; @@ -47,11 +48,17 @@ return core.Class.extend({ var tour = { name: name, current_step: parseInt(local_storage.getItem(getStepKey(name))) || 0, - skip_enabled: options.skip_enabled, steps: steps, url: options.url, test: options.test, }; + if (options.skip_enabled) { + tour.skip_link = '

' + _t('Skip tour') + '

'; + tour.skip_handler = function (tip) { + this._deactivate_tip(tip); + this._consume_tour(name); + }; + } this.tours[name] = tour; if (name === this.running_tour || (!tour.test && !_.contains(this.consumed_tours, name))) { this.active_tooltips[name] = steps[tour.current_step]; @@ -84,6 +91,7 @@ return core.Class.extend({ * active tours otherwise). Should be called each time the DOM changes. */ update: function() { + this.in_modal = this.$body.hasClass('modal-open'); if (this.running_tour) { this._check_for_tooltip(this.active_tooltips[this.running_tour], this.running_tour); } else { @@ -91,7 +99,7 @@ return core.Class.extend({ } }, _check_for_tooltip: function (tip, tour_name) { - var $trigger = $(tip.trigger).filter(':visible').first(); + var $trigger = $((this.in_modal ? '.modal ' : '') + tip.trigger).filter(':visible').first(); var extra_trigger = tip.extra_trigger ? $(tip.extra_trigger).filter(':visible').length : true; var triggered = $trigger.length && extra_trigger; if (triggered) { @@ -105,22 +113,22 @@ return core.Class.extend({ } }, _activate_tip: function(tip, tour_name, $anchor) { - var skip_enabled = this.tours[tour_name].skip_enabled; - if (skip_enabled && !tip.extra_content) { - // FIXME: not pretty but a cleaner solution will be easy when we'll stop using jquery's popover - tip.extra_content = '
' + _t('Skip tour') + ''; - } - tip.widget = new Tip(this, $anchor, tip); - tip.widget.appendTo(document.body); - tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name)); - if (skip_enabled) { - tip.widget.on('popover_clicked', this, function (event) { - if (event.target.className === 'o_skip_tour') { - this._deactivate_tip(tip); - this._consume_tour(tour_name); - } + var tour = this.tours[tour_name]; + var tip_info = tip; + if (tour.skip_link) { + tip_info = _.extend(_.omit(tip_info, 'content'), { + content: tip.content + tour.skip_link, + event_handlers: [{ + event: 'click', + selector: '.o_skip_tour', + handler: tour.skip_handler.bind(this, tip), + }], }); } + tip.widget = new Tip(this, $anchor, tip_info); + tip.widget.appendTo(document.body); + tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name)); + if (this.running_tour === tour_name) { clearTimeout(this.running_tour_timeout); if (tip.run) { diff --git a/addons/web_tour/static/src/js/tour_service.js b/addons/web_tour/static/src/js/tour_service.js index feacd2423fc..9417f4da9e1 100644 --- a/addons/web_tour/static/src/js/tour_service.js +++ b/addons/web_tour/static/src/js/tour_service.js @@ -11,7 +11,7 @@ return session.is_bound.then(function () { var tour = new TourManager(session.web_tours); // Use a MutationObserver to detect DOM changes - var untracked_classnames = ['o_tooltip', 'o_breathing']; + var untracked_classnames = ["o_tooltip", "o_tooltip_content", "o_tooltip_overlay"]; var check_tooltip = _.throttle(function (records) { var update = _.find(records, function (record) { var record_class = record.target.className; diff --git a/addons/web_tour/static/src/less/keyframes.less b/addons/web_tour/static/src/less/keyframes.less index 3f053ebd82e..08402f4b2ea 100644 --- a/addons/web_tour/static/src/less/keyframes.less +++ b/addons/web_tour/static/src/less/keyframes.less @@ -1,33 +1,49 @@ @-webkit-keyframes move-left-right { 0% { - .translate(0, 0); + .translate(-3px, 0); } 100% { - .translate(5px, 0); + .translate(3px, 0); } } @-moz-keyframes move-left-right { 0% { - .translate(0px, 0px); + .translate(-3px, 0); } 100% { - .translate(5px, 0px); + .translate(3px, 0); + } +} +@keyframes move-left-right { + 0% { + .translate(-3px, 0); + } + 100% { + .translate(3px, 0); } } @-webkit-keyframes move-bottom-top { 0% { - .translate(0, 0); + .translate(0, -3px); } 100% { - .translate(0, 5px); + .translate(0, 3px); } } @-moz-keyframes move-bottom-top { 0% { - .translate(0px, 0px); + .translate(0px, -3px); } 100% { - .translate(0, 5px); + .translate(0, 3px); + } +} +@keyframes move-bottom-top { + 0% { + .translate(0px, -3px); + } + 100% { + .translate(0, 3px); } } diff --git a/addons/web_tour/static/src/less/tip.less b/addons/web_tour/static/src/less/tip.less index 51370965f55..238ec409b1e 100644 --- a/addons/web_tour/static/src/less/tip.less +++ b/addons/web_tour/static/src/less/tip.less @@ -1,92 +1,214 @@ -@o-tip-background-color: #875a7b; .o_tooltip { - .o_breathing { - position: absolute; - width: 30px; - height: 30px; - border-radius: 50%; - z-index: @zindex-modal+1; - background-color: @o-tip-background-color; - .animation-duration(0.5s); - .animation-timing-function(ease-in); - .animation-iteration-count(infinite); - .animation-direction(alternate); + @o-tip-background-color: lighten(@odoo-brand-primary, 5%); + @o-tip-size: 26px; + @o-tip-border-width: 2px; + + @o-tip-arrow-length: floor(@o-tip-size * 0.5); + @o-tip-arrow-width: floor(@o-tip-size * 0.8); + @o-tip-arrow-integration: floor(@o-tip-size * 0.1); + + @o-tip-duration-in: 300ms; + @o-tip-size-duration-in: floor(@o-tip-duration-in * 3 / 4); + @o-tip-size-delay-in: floor(@o-tip-duration-in / 4); + @o-tip-border-radius-duration-in: floor(@o-tip-duration-in / 2); + @o-tip-border-radius-delay-in: floor(@o-tip-duration-in * 0); + @o-tip-bg-color-duration-in: floor(@o-tip-duration-in * 1); + @o-tip-bg-color-delay-in: floor(@o-tip-duration-in * 0); + @o-tip-arrow-duration-in: floor(@o-tip-duration-in / 2); + @o-tip-arrow-delay-in: floor(@o-tip-duration-in * 0); + @o-tip-white-arrow-duration-in: floor(@o-tip-duration-in * 1); + @o-tip-white-arrow-delay-in: floor(@o-tip-duration-in * 0); + + @o-tip-duration-out: 200ms; + @o-tip-size-duration-out: floor(@o-tip-duration-out * 2 / 3); + @o-tip-size-delay-out: floor(@o-tip-duration-out * 0); + @o-tip-border-radius-duration-out: floor(@o-tip-duration-out * 2 / 3); + @o-tip-border-radius-delay-out: floor(@o-tip-duration-out / 3); + @o-tip-bg-color-duration-out: floor(@o-tip-duration-out * 1); + @o-tip-bg-color-delay-out: floor(@o-tip-duration-out * 0); + @o-tip-arrow-duration-out: floor(@o-tip-duration-out / 2); + @o-tip-arrow-delay-out: floor(@o-tip-duration-out / 2); + + box-sizing: content-box; + position: absolute; + width: @o-tip-size; + height: @o-tip-size; + border: @o-tip-border-width solid @odoo-brand-primary; + border-radius: 50%; + z-index: @zindex-modal+1; + background-color: @o-tip-background-color; + box-shadow: 0 0 2px 0 @odoo-brand-lightsecondary; + + transition: + width @o-tip-size-duration-out ease @o-tip-size-delay-out, + height @o-tip-size-duration-out ease @o-tip-size-delay-out, + margin @o-tip-size-duration-out ease @o-tip-size-delay-out, + border-radius @o-tip-border-radius-duration-out ease @o-tip-border-radius-delay-out, + background-color @o-tip-bg-color-duration-out ease @o-tip-bg-color-delay-out; + + &.o_animated { + .o-animation(move-bottom-top, 500ms, ease-in, 0ms, infinite, alternate); &.right, &.left { .animation-name(move-left-right); } - &.top, &.bottom { - .animation-name(move-bottom-top); - } + } - &::after { - content: ''; - width: 0; - height: 0; + &::before { + content: ""; + border: 0 solid rgba(0, 0, 0, 0); + transition: all @o-tip-arrow-duration-out ease @o-tip-arrow-delay-out; + } + &.right::before { + .o-position-absolute(@left: -@o-tip-arrow-length+@o-tip-arrow-integration, @top: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-width/2 @o-tip-arrow-length @o-tip-arrow-width/2 0; + border-right-color: @o-tip-background-color; + } + &.top::before { + .o-position-absolute(@bottom: -@o-tip-arrow-length+@o-tip-arrow-integration, @left: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-length @o-tip-arrow-width/2 0 @o-tip-arrow-width/2; + border-top-color: @o-tip-background-color; + } + &.left::before { + .o-position-absolute(@right: -@o-tip-arrow-length+@o-tip-arrow-integration, @top: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-width/2 0 @o-tip-arrow-width/2 @o-tip-arrow-length; + border-left-color: @o-tip-background-color; + } + &.bottom::before { + .o-position-absolute(@top: -@o-tip-arrow-length+@o-tip-arrow-integration, @left: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: 0 @o-tip-arrow-width/2 @o-tip-arrow-length @o-tip-arrow-width/2; + border-bottom-color: @o-tip-background-color; + } + + &::after { + @square-size: @o-tip-size*sqrt(2)/2; + @square-pos: (@o-tip-size - @square-size)/2; + .o-position-absolute(@square-pos, @square-pos, @square-pos, @square-pos); + content: "?"; + line-height: @square-size; + text-align: center; + font-size: @square-size; + font-weight: 800; + border-color: @o-tip-background-color; + color: @odoo-brand-lightsecondary; + transition: margin @o-tip-size-duration-out ease @o-tip-size-delay-out; + } + + > .o_tooltip_overlay { + display: none; + .o-position-absolute(0, 0, 0, 0); + margin: -@o-tip-border-width; + z-index: -1; + } + > .o_tooltip_content { + padding: 8px 15px; + visibility: hidden; + transition: visibility 0s ease 0s; + color: black; + + .o_skip_tour { + display: inline-block; + margin-top: 4px; + cursor: pointer; + color: gray; + &:hover { + color: darken(gray, 20%); + } } - &.right::after { - .o-position-absolute(@left: -4px, @top: 5px); - border-right: 10px solid @o-tip-background-color; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - } - &.top::after { - .o-position-absolute(@bottom: -4px, @left: 5px); - border-top: 10px solid @o-tip-background-color; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - } - &.left::after { - .o-position-absolute(@right: -4px, @bottom: 5px); - border-left: 10px solid @o-tip-background-color; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - } - &.bottom::after { - .o-position-absolute(@top: -4px, @right: 5px); - border-bottom: 10px solid @o-tip-background-color; - border-right: 10px solid transparent; - border-left: 10px solid transparent; + > p:last-child { + margin-bottom: 0; } + } + + &.active { + border-radius: 0; + background-color: white; + + transition: + width @o-tip-size-duration-in ease @o-tip-size-delay-in, + height @o-tip-size-duration-in ease @o-tip-size-delay-in, + margin @o-tip-size-duration-in ease @o-tip-size-delay-in, + border-radius @o-tip-border-radius-duration-in ease @o-tip-border-radius-delay-in, + background-color @o-tip-bg-color-duration-in ease @o-tip-bg-color-delay-in; &::before { - .o-position-absolute(@top: 10px, @left: 10px); - content: '?'; - width: 10px; - height: 10px; - font-size: 18px; - font-weight: 800; - line-height: 0.6; - color: lightgray; - border-radius: 50%; + transition: all @o-tip-arrow-duration-in ease @o-tip-arrow-delay-in; + } + &::after { + content: ""; + border: 0 solid rgba(0, 0, 0, 0); + } + + > .o_tooltip_overlay { + display: block; + } + > .o_tooltip_content { + visibility: visible; + transition: visibility 0s ease @o-tip-duration-in; + } + + &.right { + &::before { + left: -@o-tip-arrow-length; + border-right-color: @odoo-brand-primary; + } + &::after { + .o-position-absolute(@left: -@o-tip-arrow-length+@o-tip-border-width*2, @top: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-width/2 @o-tip-arrow-length @o-tip-arrow-width/2 0; + border-right-color: white; + transition: border-right-color @o-tip-white-arrow-duration-in ease @o-tip-white-arrow-delay-in; + } + } + &.top { + &::before { + bottom: -@o-tip-arrow-length; + border-top-color: @odoo-brand-primary; + } + &::after { + .o-position-absolute(@bottom: -@o-tip-arrow-length+@o-tip-border-width*2, @left: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-length @o-tip-arrow-width/2 0 @o-tip-arrow-width/2; + border-top-color: white; + transition: border-top-color @o-tip-white-arrow-duration-in ease @o-tip-white-arrow-delay-in; + } + } + &.left { + &::before { + right: -@o-tip-arrow-length; + border-left-color: @odoo-brand-primary; + } + &::after { + .o-position-absolute(@right: -@o-tip-arrow-length+@o-tip-border-width*2, @top: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: @o-tip-arrow-width/2 0 @o-tip-arrow-width/2 @o-tip-arrow-length; + border-left-color: white; + transition: border-left-color @o-tip-white-arrow-duration-in ease @o-tip-white-arrow-delay-in; + } + } + &.bottom { + &::before { + top: -@o-tip-arrow-length; + border-bottom-color: @odoo-brand-primary; + } + &::after { + .o-position-absolute(@top: -@o-tip-arrow-length+@o-tip-border-width*2, @left: (@o-tip-size - @o-tip-arrow-width) / 2); + border-width: 0 @o-tip-arrow-width/2 @o-tip-arrow-length @o-tip-arrow-width/2; + border-bottom-color: white; + transition: border-bottom-color @o-tip-white-arrow-duration-in ease @o-tip-white-arrow-delay-in; + } } } - .popover { - border: 2px solid @odoo-brand-primary; - &.right > .arrow { - border-right-color: @odoo-brand-primary; - left: -13px; - &::after { - border-left-width: 2px; + &.inverse { + &.left, &.right { + &::before, &.active::after { + top: auto; + bottom: (@o-tip-size - @o-tip-arrow-width) / 2; } } - &.bottom > .arrow { - border-bottom-color: @odoo-brand-primary; - top: -13px; - &::after { - border-top-width: 2px; + &.top, &.bottom { + &::before, &.active::after { + left: auto; + right: (@o-tip-size - @o-tip-arrow-width) / 2; } } - .popover-content { - color: black; - text-align: justify; - .o_skip_tour { - display: inline-block; - margin-top: 5px; - cursor: pointer; - color: gray; - } - } } } diff --git a/addons/web_tour/static/src/xml/tip.xml b/addons/web_tour/static/src/xml/tip.xml index b8109646dcd..8874b5b72e8 100644 --- a/addons/web_tour/static/src/xml/tip.xml +++ b/addons/web_tour/static/src/xml/tip.xml @@ -1,10 +1,9 @@ - - -
-
+
+
+
+ +
- -