From 1c8e3c315686646d62e05cf11c7cac01a7ce7d0f Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 18 Jun 2014 12:25:53 +0200 Subject: [PATCH] [IMP] Tour: add tour in web module; tour became available in backend. --- addons/web/static/src/js/tour.js | 540 +++++++++++++++++++++ addons/web/static/src/xml/website.tour.xml | 27 ++ 2 files changed, 567 insertions(+) create mode 100644 addons/web/static/src/js/tour.js create mode 100644 addons/web/static/src/xml/website.tour.xml diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js new file mode 100644 index 00000000000..634fa5c9421 --- /dev/null +++ b/addons/web/static/src/js/tour.js @@ -0,0 +1,540 @@ +(function () { + 'use strict'; + +// raise an error in test mode if openerp don't exist +if (typeof openerp === "undefined") { + var error = "openerp is undefined" + + "\nhref: " + window.location.href + + "\nreferrer: " + document.referrer + + "\nlocalStorage: " + window.localStorage.getItem("tour"); + if (typeof $ !== "undefined") { + error += '\n\n' + $("body").html(); + } + throw new Error(error); +} + +var website = openerp.website; + +// don't rewrite T in test mode +if (typeof openerp.Tour !== "undefined") { + return; +} + +///////////////////////////////////////////////// + + +/* jQuery selector to match exact text inside an element + * :containsExact() - case insensitive + * :containsExactCase() - case sensitive + * :containsRegex() - set by user ( use: $(el).find(':containsRegex(/(red|blue|yellow)/gi)') ) + */ +$.extend($.expr[':'],{ + containsExact: function(a,i,m){ + return $.trim(a.innerHTML.toLowerCase()) === m[3].toLowerCase(); + }, + containsExactCase: function(a,i,m){ + return $.trim(a.innerHTML) === m[3]; + }, + // Note all escaped characters need to be double escaped + // inside of the containsRegex, so "\(" needs to be "\\(" + containsRegex: function(a,i,m){ + var regreg = /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})$/, + reg = regreg.exec(m[3]); + return reg ? new RegExp(reg[1], reg[2]).test($.trim(a.innerHTML)) : false; + } +}); +$.ajaxSetup({ + beforeSend:function(){ + $.ajaxBusy = ($.ajaxBusy|0) + 1; + }, + complete:function(){ + $.ajaxBusy--; + } +}); + +///////////////////////////////////////////////// + +var localStorage = window.localStorage; + +var Tour = { + tours: {}, + defaultDelay: 50, + retryRunningDelay: 1000, + errorDelay: 5000, + state: null, + $element: null, + timer: null, + testtimer: null, + currentTimer: null, + register: function (tour) { + if (tour.mode !== "test") tour.mode = "tutorial"; + Tour.tours[tour.id] = tour; + }, + run: function (tour_id, mode) { + var tour = Tour.tours[tour_id]; + if (!tour) { + throw new Error("Can't run '"+tour_id+"' (tour undefined)"); + } + this.time = new Date().getTime(); + if (tour.path && !window.location.href.match(new RegExp("("+Tour.getLang()+")?"+tour.path+"#?$", "i"))) { + var href = "/"+Tour.getLang()+tour.path; + console.log("Tour Begin from run method (redirection to "+href+")"); + Tour.saveState(tour.id, mode || tour.mode, -1, 0); + window.location.href = href; + } else { + console.log("Tour Begin from run method"); + Tour.saveState(tour.id, mode || tour.mode, 0, 0); + Tour.running(); + } + }, + registerSteps: function (tour) { + if (tour.register) { + return; + } + tour.register = true; + + for (var index=0, len=tour.steps.length; index 0 && tour.steps[index-1] && + tour.steps[index-1].popover && tour.steps[index-1].popover.next) { + step.waitNot = '.popover.tour.fade.in:visible'; + } + if (!step.waitFor && index > 0 && tour.steps[index-1].snippet) { + step.waitFor = '.oe_overlay_options .oe_options:visible'; + } + + + var snippet = step.element && step.element.match(/#oe_snippets (.*) \.oe_snippet_thumbnail/); + if (snippet) { + step.snippet = snippet[1]; + } else if (step.snippet) { + step.element = '#oe_snippets '+step.snippet+' .oe_snippet_thumbnail'; + } + + if (!step.element) { + step.element = "body"; + step.orphan = true; + step.backdrop = true; + } + } + if (tour.steps[index-1] && + tour.steps[index-1].popover && tour.steps[index-1].popover.next) { + var step = { + _title: "", + id: index, + waitNot: '.popover.tour.fade.in:visible' + }; + tour.steps.push(step); + } + + // rendering bootstrap tour and popover + if (tour.mode !== "test") { + for (var index=0, len=tour.steps.length; index'); + } + + if (step.backdrop || $element.parents("#website-top-navbar, .oe_navbar, .modal").size()) { + $tip.css("z-index", 2010); + } + + // button click event + $tip.find("button") + .one("click", function () { + step.busy = true; + if (!$(this).is("[data-role='next']")) { + clearTimeout(Tour.timer); + Tour.endTour(); + } + Tour.closePopover(); + }); + + Tour.repositionPopover(); + }, + repositionPopover: function() { + var popover = Tour.$element.data("bs.popover"); + var $tip = Tour.$element.data("bs.popover").tip(); + + if (popover.options.orphan) { + return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2); + } + + var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset; + offsetWidth = $tip[0].offsetWidth; + offsetHeight = $tip[0].offsetHeight; + tipOffset = $tip.offset(); + originalLeft = tipOffset.left; + originalTop = tipOffset.top; + offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight(); + if (offsetBottom < 0) { + tipOffset.top = tipOffset.top + offsetBottom; + } + offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth(); + if (offsetRight < 0) { + tipOffset.left = tipOffset.left + offsetRight; + } + if (tipOffset.top < 0) { + tipOffset.top = 0; + } + if (tipOffset.left < 0) { + tipOffset.left = 0; + } + $tip.offset(tipOffset); + if (popover.options.placement === "bottom" || popover.options.placement === "top") { + var left = Tour.$element.offset().left + Tour.$element.outerWidth()/2 - tipOffset.left; + $tip.find(".arrow").css("left", left ? left + "px" : ""); + } else if (popover.options.placement !== "auto") { + var top = Tour.$element.offset().top + Tour.$element.outerHeight()/2 - tipOffset.top; + $tip.find(".arrow").css("top", top ? top + "px" : ""); + } + }, + _load_template: false, + load_template: function () { + // don't need template to use bootstrap Tour in automatic mode + Tour._load_template = true; + if (typeof QWeb2 === "undefined") return $.when(); + var def = $.Deferred(); + openerp.qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) { + if (err) { + def.reject(err); + } else { + def.resolve(); + } + }); + return def; + }, + popoverTitle: function (tour, options) { + return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover_title', options) : options.title; + }, + popover: function (options) { + return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover', options) : options.title; + }, + getLang: function () { + return $("html").attr("lang").replace(/-/, '_'); + }, + getState: function () { + var state = JSON.parse(localStorage.getItem("tour") || 'false') || {}; + if (state) { this.time = state.time; } + var tour_id,mode,step_id; + if (!state.id && window.location.href.indexOf("#tutorial.") > -1) { + state = { + "id": window.location.href.match(/#tutorial\.(.*)=true/)[1], + "mode": "tutorial", + "step_id": 0 + }; + window.location.hash = ""; + console.log("Tour Begin from url hash"); + Tour.saveState(state.id, state.mode, state.step_id, 0); + } + if (!state.id) { + return; + } + state.tour = Tour.tours[state.id]; + state.step = state.tour && state.tour.steps[state.step_id === -1 ? 0 : state.step_id]; + return state; + }, + error: function (step, message) { + var state = Tour.getState(); + message += '\n tour: ' + state.id + + '\n step: ' + step.id + ": '" + (step._title || step.title) + "'" + + '\n href: ' + window.location.href + + '\n referrer: ' + document.referrer + + '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) + + '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size()) + + '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size()) + + "\n localStorage: " + JSON.stringify(localStorage) + + '\n\n' + $("body").html(); + Tour.reset(); + throw new Error(message); + }, + lists: function () { + var tour_ids = []; + for (var k in Tour.tours) { + tour_ids.push(k); + } + return tour_ids; + }, + saveState: function (tour_id, mode, step_id, number, wait) { + localStorage.setItem("tour", JSON.stringify({ + "id":tour_id, + "mode":mode, + "step_id":step_id || 0, + "time": this.time, + "number": number+1, + "wait": wait || 0 + })); + }, + reset: function () { + var state = Tour.getState(); + if (state) { + for (var k in state.tour.steps) { + state.tour.steps[k].busy = false; + } + } + localStorage.removeItem("tour"); + clearTimeout(Tour.timer); + clearTimeout(Tour.testtimer); + Tour.closePopover(); + }, + running: function () { + function run () { + var state = Tour.getState(); + if (!state) return; + if (!Tour._load_template) { + Tour.load_template().then(Tour.running); + } + else if (state.tour) { + console.log("Tour '"+state.id+"' is running"); + Tour.registerSteps(state.tour); + Tour.nextStep(); + } else { + if (state.wait > 10) { + Tour.error(step, "Tour '"+state.id+"' undefined"); + } + Tour.saveState(state.id, state.mode, step.id, state.number, state.wait+1); + console.log("Tour '"+state.id+"' wait for running (tour undefined)"); + setTimeout(Tour.running, state.mode === "test" ? Tour.defaultDelay : Tour.retryRunningDelay); + } + } + setTimeout(function () { + if ($.ajaxBusy) { + $(document).ajaxStop(run); + } else { + run(); + } + },0); + }, + check: function (step) { + return (step && + (!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) && + (!step.waitNot || !$(step.waitNot).size()) && + (!step.waitFor || $(step.waitFor).size())); + }, + waitNextStep: function () { + var state = Tour.getState(); + var time = new Date().getTime(); + var timer; + var next = state.tour.steps[state.step.id+1]; + var overlaps = state.mode === "test" ? Tour.errorDelay : 0; + + window.onbeforeunload = function () { + clearTimeout(Tour.timer); + clearTimeout(Tour.testtimer); + }; + + function checkNext () { + Tour.autoTogglePopover(); + + clearTimeout(Tour.timer); + if (Tour.check(next)) { + clearTimeout(Tour.currentTimer); + // use an other timeout for cke dom loading + Tour.saveState(state.id, state.mode, state.step.id, 0); + setTimeout(function () { + Tour.nextStep(next); + }, Tour.defaultDelay); + } else if (!overlaps || new Date().getTime() - time < overlaps) { + Tour.timer = setTimeout(checkNext, Tour.defaultDelay); + } else { + Tour.error(next, "Can't reach the next step"); + } + } + checkNext(); + }, + nextStep: function (step) { + var state = Tour.getState(); + + if (!state) { + return; + } + + step = step || state.step; + var next = state.tour.steps[step.id+1]; + + if (state.number > 3) { + Tour.error(next, "Cycling. Can't reach the next step"); + } + + Tour.saveState(state.id, state.mode, step.id, state.number); + + if (step.id !== state.step_id) { + console.log("Tour Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)"); + } + + Tour.autoTogglePopover(true); + + if (step.onload) { + step.onload(); + } + + if (next) { + setTimeout(function () { + Tour.waitNextStep(); + if (state.mode === "test") { + setTimeout(function(){ + Tour.autoNextStep(state.tour, step); + }, Tour.defaultDelay); + } + }, next.wait || 0); + } else { + Tour.endTour(); + } + }, + endTour: function () { + var state = Tour.getState(); + var test = state.step.id >= state.tour.steps.length-1; + Tour.reset(); + if (test) { + console.log('ok'); + } else { + console.log('error'); + } + }, + autoNextStep: function (tour, step) { + clearTimeout(Tour.testtimer); + + function autoStep () { + if (!step) return; + + if (step.autoComplete) { + step.autoComplete(tour); + } + + $(".popover.tour [data-role='next']").click(); + + var $element = $(step.element); + if (!$element.size()) return; + + if (step.snippet) { + + Tour.autoDragAndDropSnippet($element); + + } else if ($element.is(":visible")) { + + $element.trigger($.Event("mouseenter", { srcElement: $element[0] })); + $element.trigger($.Event("mousedown", { srcElement: $element[0] })); + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + $element[0].dispatchEvent(evt); + + // trigger after for step like: mouseenter, next step click on button display with mouseenter + setTimeout(function () { + $element.trigger($.Event("mouseup", { srcElement: $element[0] })); + $element.trigger($.Event("mouseleave", { srcElement: $element[0] })); + }, 1000); + } + if (step.sampleText) { + + $element.trigger($.Event("keydown", { srcElement: $element })); + if ($element.is("input") ) { + $element.val(step.sampleText); + } if ($element.is("select")) { + $element.find("[value='"+step.sampleText+"'], option:contains('"+step.sampleText+"')").attr("selected", true); + $element.val(step.sampleText); + } else { + $element.html(step.sampleText); + } + setTimeout(function () { + $element.trigger($.Event("keyup", { srcElement: $element })); + $element.trigger($.Event("change", { srcElement: $element })); + }, self.defaultDelay<<1); + + } + } + Tour.testtimer = setTimeout(autoStep, 100); + }, + autoDragAndDropSnippet: function (selector) { + var $thumbnail = $(selector).first(); + var thumbnailPosition = $thumbnail.position(); + $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top })); + $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: document.body.scrollWidth/2, pageY: document.body.scrollHeight/2 })); + var $dropZone = $(".oe_drop_zone").first(); + var dropPosition = $dropZone.position(); + $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top })); + } +}; +openerp.Tour = Tour; + +///////////////////////////////////////////////// + +$(document).ready(Tour.running); + +}()); diff --git a/addons/web/static/src/xml/website.tour.xml b/addons/web/static/src/xml/website.tour.xml new file mode 100644 index 00000000000..d3d3fb8c57a --- /dev/null +++ b/addons/web/static/src/xml/website.tour.xml @@ -0,0 +1,27 @@ + + + +
+
+

+
+ + + +
+
+ + + +