580 lines
20 KiB
JavaScript
580 lines
20 KiB
JavaScript
(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,
|
|
autoRunning: true,
|
|
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) {
|
|
return Tour.error(null, "Can't run '"+tour_id+"' (tour undefined)");
|
|
}
|
|
Tour.log("Tour '"+tour_id+"' Begin from run method", true);
|
|
var state = Tour.getState();
|
|
if (state) {
|
|
if (state.mode === "test") {
|
|
return Tour.error(false, "An other running tour has been detected all tours are now killed.");
|
|
} else {
|
|
Tour.endTour();
|
|
}
|
|
}
|
|
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;
|
|
Tour.saveState(tour.id, mode || tour.mode, -1, 0);
|
|
$(document).one("ajaxStop", Tour.running);
|
|
window.location.href = href;
|
|
} else {
|
|
Tour.saveState(tour.id, mode || tour.mode, 0, 0);
|
|
Tour.running();
|
|
}
|
|
},
|
|
registerSteps: function (tour, mode) {
|
|
if (tour.register) {
|
|
return;
|
|
}
|
|
tour.register = true;
|
|
|
|
for (var index=0, len=tour.steps.length; index<len; index++) {
|
|
var step = tour.steps[index];
|
|
step.id = index;
|
|
|
|
if (!step.waitNot && 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;
|
|
} else {
|
|
step.popover = step.popover || {};
|
|
step.popover.arrow = true;
|
|
}
|
|
}
|
|
if (tour.steps[index-1] &&
|
|
tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
|
|
var step = {
|
|
_title: "close popover and finish",
|
|
id: index,
|
|
waitNot: '.popover.tour.fade.in:visible'
|
|
};
|
|
tour.steps.push(step);
|
|
}
|
|
|
|
// rendering bootstrap tour and popover
|
|
if (mode !== "test") {
|
|
for (var index=0, len=tour.steps.length; index<len; index++) {
|
|
var step = tour.steps[index];
|
|
step._title = step._title || step.title;
|
|
step.title = Tour.popoverTitle(tour, { title: step._title });
|
|
step.template = step.template || Tour.popover( step.popover );
|
|
}
|
|
}
|
|
},
|
|
closePopover: function () {
|
|
if (Tour.$element) {
|
|
Tour.$element.popover('destroy');
|
|
Tour.$element.removeData("tour");
|
|
Tour.$element.removeData("tour-step");
|
|
$(".tour-backdrop").remove();
|
|
$(".popover.tour").remove();
|
|
Tour.$element = null;
|
|
}
|
|
},
|
|
autoTogglePopover: function () {
|
|
var state = Tour.getState();
|
|
var step = state.step;
|
|
|
|
if (Tour.$element &&
|
|
Tour.$element.is(":visible") &&
|
|
Tour.$element.data("tour") === state.id &&
|
|
Tour.$element.data("tour-step") === step.id) {
|
|
Tour.repositionPopover();
|
|
return;
|
|
}
|
|
|
|
if (step.busy) {
|
|
return;
|
|
}
|
|
|
|
Tour.closePopover();
|
|
|
|
var $element = $(step.element).first();
|
|
if (!step.element || !$element.size() || !$element.is(":visible")) {
|
|
return;
|
|
}
|
|
|
|
|
|
Tour.$element = $element;
|
|
$element.data("tour", state.id);
|
|
$element.data("tour-step", step.id);
|
|
$element.popover({
|
|
placement: step.placement || "auto",
|
|
animation: true,
|
|
trigger: "manual",
|
|
title: step.title,
|
|
content: step.content,
|
|
html: true,
|
|
container: "body",
|
|
template: step.template,
|
|
orphan: step.orphan
|
|
}).popover("show");
|
|
|
|
|
|
var $tip = $element.data("bs.popover").tip();
|
|
|
|
|
|
// add popover style (orphan, static, backdrop)
|
|
if (step.orphan) {
|
|
$tip.addClass("orphan");
|
|
}
|
|
|
|
var node = $element[0];
|
|
var css;
|
|
do {
|
|
css = window.getComputedStyle(node);
|
|
if (!css || css.position == "fixed") {
|
|
$tip.addClass("fixed");
|
|
break;
|
|
}
|
|
} while ((node = node.parentNode) && node !== document);
|
|
|
|
if (step.backdrop) {
|
|
$("body").append('<div class="tour-backdrop"></div>');
|
|
}
|
|
|
|
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 = popover.tip();
|
|
|
|
if (popover.options.orphan) {
|
|
return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
|
|
}
|
|
|
|
if (Tour.$element.parents("div").filter(function(){ return getComputedStyle(this).position === 'fixed'; }).length) {
|
|
var pos = popover.getPosition();
|
|
var top = pos.top;
|
|
if (popover.options.placement === "top") top -= $tip.height();
|
|
if (popover.options.placement === "bottom") top += pos.height;
|
|
$tip.css({'top': top+'px'});
|
|
}
|
|
|
|
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;
|
|
popover.arrow().css("left", left ? left + "px" : "");
|
|
} else if (popover.options.placement !== "auto") {
|
|
var top = Tour.$element.offset().top + Tour.$element.outerHeight()/2 - tipOffset.top;
|
|
popover.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") ? "/" + $("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 = "";
|
|
Tour.log("Tour '"+state.id+"' 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;
|
|
},
|
|
log: function (message, add_user) {
|
|
if (add_user) {
|
|
var user = $(".navbar .dropdown:has(>.js_usermenu) a:first, .navbar .oe_topbar_name, .pos .username").text();
|
|
if (!user && $('a[href*="/login"]')) user = 'Public User';
|
|
message += " (" + (user||"").replace(/^\s*|\s*$/g, '') + ")";
|
|
}
|
|
console.log(message);
|
|
},
|
|
error: function (step, message) {
|
|
var state = Tour.getState();
|
|
console.log(state.tour.steps.slice());
|
|
message += '\n tour: ' + state.id
|
|
+ (step ? '\n step: ' + step.id + ": '" + (step._title || step.title) + "'" : '' )
|
|
+ '\n href: ' + window.location.href
|
|
+ '\n referrer: ' + document.referrer
|
|
+ (step ? '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) : '' )
|
|
+ (step ? '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size()) : '' )
|
|
+ (step ? '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size()) : '' )
|
|
+ "\n localStorage: " + JSON.stringify(localStorage)
|
|
+ '\n\n' + $("body").html();
|
|
Tour.log(message, true);
|
|
Tour.endTour();
|
|
},
|
|
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 && state.tour) {
|
|
for (var k in state.tour.steps) {
|
|
state.tour.steps[k].busy = false;
|
|
}
|
|
}
|
|
localStorage.removeItem("tour");
|
|
clearTimeout(Tour.timer);
|
|
clearTimeout(Tour.testtimer);
|
|
Tour.closePopover();
|
|
Tour.log("Tour reset");
|
|
},
|
|
running: function () {
|
|
var state = Tour.getState();
|
|
if (!state) return;
|
|
else if (state.tour) {
|
|
if (!Tour._load_template) {
|
|
Tour.load_template().then(Tour.running);
|
|
return;
|
|
}
|
|
Tour.log("Tour '"+state.id+"' is running", true);
|
|
Tour.registerSteps(state.tour, state.mode);
|
|
Tour.nextStep();
|
|
} else {
|
|
if (state.mode === "test" && state.wait >= 10) {
|
|
return Tour.error(state.step, "Tour '"+state.id+"' undefined");
|
|
}
|
|
Tour.saveState(state.id, state.mode, state.step_id, state.number-1, state.wait+1);
|
|
Tour.log("Tour '"+state.id+"' wait for running (tour undefined)");
|
|
setTimeout(Tour.running, Tour.retryRunningDelay);
|
|
}
|
|
},
|
|
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 () {
|
|
if (!Tour.getState()) return;
|
|
|
|
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 {
|
|
return 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.mode === "test" && state.number > 3) {
|
|
return 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) {
|
|
Tour.log("Tour '"+state.id+"' Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)");
|
|
}
|
|
|
|
Tour.autoTogglePopover(true);
|
|
|
|
if (step.onload) {
|
|
step.onload();
|
|
}
|
|
|
|
if (next) {
|
|
setTimeout(function () {
|
|
if (Tour.getState()) {
|
|
Tour.waitNextStep();
|
|
}
|
|
if (state.mode === "test") {
|
|
setTimeout(function(){
|
|
Tour.autoNextStep(state.tour, step);
|
|
}, Tour.defaultDelay);
|
|
}
|
|
}, next.wait || 0);
|
|
} else {
|
|
setTimeout(function(){
|
|
Tour.autoNextStep(state.tour, step);
|
|
}, Tour.defaultDelay);
|
|
Tour.endTour();
|
|
}
|
|
},
|
|
endTour: function () {
|
|
var state = Tour.getState();
|
|
var test = state.step && state.step.id >= state.tour.steps.length-1;
|
|
Tour.reset();
|
|
if (test) {
|
|
Tour.log("Tour '"+state.id+"' finish: ok");
|
|
Tour.log('ok');
|
|
} else {
|
|
Tour.log("Tour '"+state.id+"' finish: error");
|
|
Tour.log('error');
|
|
}
|
|
},
|
|
autoNextStep: function (tour, step) {
|
|
clearTimeout(Tour.testtimer);
|
|
|
|
function autoStep () {
|
|
if (!Tour.getState()) return;
|
|
|
|
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 () {
|
|
if (!Tour.getState()) return;
|
|
$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 () {
|
|
if (!Tour.getState()) return;
|
|
$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(function () {
|
|
if (Tour.autoRunning) {
|
|
Tour.running();
|
|
};
|
|
});
|
|
|
|
}());
|