4 var website = openerp.website;
5 website.add_template_file('/website/static/src/xml/website.tour.xml');
7 website.Tour = openerp.Class.extend({
9 tourStorage: window.localStorage, // FIXME: will break on iPad in private mode
11 this.tour = new Tour({
13 storage: this.tourStorage,
15 template: this.popover(),
17 window.scrollTo(0, 0);
22 registerStep: function (step) {
24 step.title = openerp.qweb.render('website.tour_popover_title', { title: step.title });
29 step.element = '#oe_snippets div.oe_snippet[data-snippet-id="'+step.snippet+'"] .oe_snippet_thumbnail';
32 if (step.trigger === 'click') {
33 step.triggers = function (callback) {
34 $(step.element).one('click', function () {
35 (callback || self.moveToNextStep).apply(self);
38 } else if (step.trigger === 'reload') {
39 step.triggers = function (callback) {
40 var stack = JSON.parse(localStorage.getItem("website-reloads")) || [];
41 var index = stack.indexOf(step.stepId);
43 stack.splice(index,1);
44 (callback || self.moveToNextStep).apply(self);
46 stack.push(step.stepId);
48 localStorage.setItem("website-reloads", JSON.stringify(stack));
50 } else if (step.trigger.url) {
51 step.triggers = function (callback) {
52 var stack = JSON.parse(localStorage.getItem("website-geturls")) || [];
53 var id = step.trigger.url.toString();
54 var index = stack.indexOf(id);
56 var url = new website.UrlParser(window.location.href);
57 var test = typeof step.trigger.url === "string" ?
58 step.trigger.url == url.pathname+url.search :
59 step.trigger.url.test(url.pathname+url.search);
61 stack.splice(index,1);
62 (callback || self.moveToNextStep).apply(self);
66 localStorage.setItem("website-geturls", JSON.stringify(stack));
68 } else if (step.trigger === 'drag') {
69 step.triggers = function (callback) {
70 self.onSnippetDragged(callback || self.moveToNextStep);
72 } else if (step.trigger.id) {
73 if (step.trigger.emitter && step.trigger.type === 'openerp') {
74 step.triggers = function (callback) {
75 step.trigger.emitter.on(step.trigger.id, self, function customHandler () {
76 step.trigger.emitter.off(step.trigger.id, customHandler);
77 (callback || self.moveToNextStep).apply(self, arguments);
81 step.triggers = function (callback) {
82 var emitter = _.isString(step.trigger.emitter) ? $(step.trigger.emitter) : (step.trigger.emitter || $(step.element));
83 if (!emitter.size()) throw "Emitter is undefined";
84 emitter.on(step.trigger.id, function () {
85 (callback || self.moveToNextStep).apply(self, arguments);
89 } else if (step.trigger.modal) {
90 step.triggers = function (callback) {
91 var $doc = $(document);
93 if (step.trigger.modal.stopOnClose) {
97 $doc.on('hide.bs.modal', onStop);
98 $doc.one('shown.bs.modal', function () {
99 $('.modal button.btn-primary').one('click', function () {
100 $doc.off('hide.bs.modal', onStop);
101 (callback || self.moveToNextStep).apply(self, [step.trigger.modal.afterSubmit]);
103 (callback || self.moveToNextStep).apply(self);
108 step.onShow = (function () {
109 var executed = false;
112 _.isFunction(step.onStart) && step.onStart();
113 _.isFunction(step.triggers) && step.triggers();
120 registerSteps: function () {
122 this.tour.addSteps(_.map(this.steps, function (step) {
123 return self.registerStep(step);
127 this.tourStorage.removeItem(this.id+'_current_step');
128 this.tourStorage.removeItem(this.id+'_end');
129 this.tour._current = 0;
130 $('.popover.tour').remove();
133 if (this.resume() || ((this.currentStepIndex() === 0) && !this.tour.ended())) {
137 currentStepIndex: function () {
138 var index = this.tourStorage.getItem(this.id+'_current_step') || 0;
139 return parseInt(index, 10);
141 indexOfStep: function (stepId) {
143 _.each(this.steps, function (step, i) {
144 if (step.stepId === stepId) {
150 isCurrentStep: function (stepId) {
151 return this.currentStepIndex() === this.indexOfStep(stepId);
153 moveToStep: function (step) {
154 var index = _.isNumber(step) ? step : this.indexOfStep(step);
155 if (index >= this.steps.length) {
157 } else if (index >= 0) {
159 setTimeout(function () {
160 $('.popover.tour').remove();
161 setTimeout(function () {
162 self.tour.goto(index);
167 moveToNextStep: function () {
168 var nextStepIndex = this.currentStepIndex() + 1;
169 this.moveToStep(nextStepIndex);
174 redirect: function (url) {
175 url = url || new website.UrlParser(window.location.href);
176 var path = (this.path && url.pathname !== this.path) ? this.path : url.pathname;
177 var search = url.activateTutorial(this.id);
178 var newUrl = path + search;
179 window.location.replace(newUrl);
182 return this.tourStorage.getItem(this.id+'_end') === "yes";
184 resume: function () {
185 // Override if necessary
186 return this.tourStorage.getItem(this.id+'_current_step') && !this.ended();
188 trigger: function (url) {
189 // Override if necessary
190 url = url || new website.UrlParser(window.location.href);
191 return url.isActive(this.id);
193 testUrl: function (pattern) {
194 var url = new website.UrlParser(window.location.href);
195 return pattern.test(url.pathname+url.search);
197 popover: function (options) {
198 return openerp.qweb.render('website.tour_popover', options);
200 onSnippetDragged: function (callback) {
202 function beginDrag () {
203 $('.popover.tour').remove();
204 function advance () {
205 if (_.isFunction(callback)) {
206 callback.apply(self);
209 $(document.body).one('mouseup', advance);
211 $('#website-top-navbar [data-snippet-id].ui-draggable').one('mousedown', beginDrag);
213 onSnippetDraggedAdvance: function () {
214 onSnippetDragged(self.moveToNextStep);
218 website.UrlParser = openerp.Class.extend({
219 init: function (url) {
220 var a = document.createElement('a');
224 this.protocol = a.protocol;
226 this.hostname = a.hostname;
227 this.pathname = a.pathname;
228 this.origin = a.origin;
229 this.search = a.search;
231 function generateTrigger (id) {
232 return "tutorial."+id+"=true";
234 this.activateTutorial = function (id) {
235 var urlTrigger = generateTrigger(id);
236 var querystring = _.filter(this.search.split('?'), function (str) {
239 if (querystring.length > 0) {
240 var queries = _.filter(querystring[0].split("&"), function (query) {
241 return query.indexOf("tutorial.") < 0
243 queries.push(urlTrigger);
244 return "?"+_.uniq(queries).join("&");
246 return "?"+urlTrigger;
249 this.isActive = function (id) {
250 var urlTrigger = generateTrigger(id);
251 return this.search.indexOf(urlTrigger) >= 0;
256 var TestConsole = openerp.Class.extend({
259 init: function (editor) {
261 throw new Error("Editor cannot be null or undefined");
263 this.editor = editor;
265 test: function (id) {
266 return _.find(this.tests, function (tour) {
267 return tour.id === id;
270 snippetSelector: function (snippetId) {
271 return '#oe_snippets div.oe_snippet[data-snippet-id="'+snippetId+'"] .oe_snippet_thumbnail';
273 snippetThumbnail: function (snippetId) {
274 return $(this.snippetSelector(snippetId)).first();
276 snippetThumbnailExists: function (snippetId) {
277 return this.snippetThumbnail(snippetId).length > 0;
279 dragAndDropSnippet: function (snippetId) {
280 function actualDragAndDrop ($thumbnail) {
281 var thumbnailPosition = $thumbnail.position();
282 $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top }));
283 $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top+500 }));
284 var $dropZone = $(".oe_drop_zone").first();
285 var dropPosition = $dropZone.position();
286 $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
288 if (this.snippetThumbnailExists(snippetId)) {
289 actualDragAndDrop(this.snippetThumbnail(snippetId));
291 this.editor.on('rte:ready', this, function () {
292 actualDragAndDrop(this.snippetThumbnail(snippetId));
299 website.EditorBar.include({
302 var result = this._super();
303 website.TestConsole = new TestConsole(this);
307 $('.tour-backdrop').click(function (e) {
308 e.stopImmediatePropagation();
311 var menu = $('#help-menu');
312 _.each(this.tours, function (tour) {
313 var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
314 $menuItem.click(function () {
315 tour.redirect(new website.UrlParser(window.location.href));
319 menu.append($menuItem);
320 if (tour.trigger()) {
324 return this._super();
326 registerTour: function (tour) {
328 var testId = 'test_'+tour.id+'_tour';
329 this.tours.push(tour);
330 var defaultDelay = 500; //ms
334 run: function (force) {
335 if (force === true) {
338 var actionSteps = _.filter(tour.steps, function (step) {
339 return step.trigger || step.sampleText;
341 window.onbeforeunload = function () {
342 clearTimeout(overlapsCrash);
344 function throwError (message) {
345 console.log(JSON.parse(window.localStorage.getItem("test-report")));
349 function executeStep (step) {
350 // check if they are a cycle
351 var lastStep = window.localStorage.getItem(testId);
352 var tryStep = lastStep != step.stepId ? 0 : (+(window.localStorage.getItem("test-last-"+testId) || 0) + 1);
353 window.localStorage.setItem("test-last-"+testId, tryStep);
355 throwError("Test: '" + testId + "' cycling step: '" + step.stepId + "'");
358 // set last time for report
359 if (tryStep == 0 || !window.localStorage.getItem("test-last-time")) {
360 window.localStorage.setItem("test-last-time", new Date().getTime());
364 window.localStorage.setItem(testId, step.stepId);
366 clearTimeout(overlapsCrash);
370 var report = JSON.parse(window.localStorage.getItem("test-report")) || {};
371 report[step.stepId] = (new Date().getTime() - window.localStorage.getItem("test-last-time")) + " ms";
372 window.localStorage.setItem("test-report", JSON.stringify(report));
374 var nextStep = actionSteps.shift();
376 setTimeout(function () {
377 executeStep(nextStep);
378 }, step.delay || defaultDelay);
380 window.localStorage.removeItem(testId);
384 overlapsCrash = setTimeout(function () {
385 throwError("Test: '" + testId + "' can't resolve step: '" + step.stepId + "'");
386 }, (step.delay || defaultDelay) + 1000);
388 setTimeout(function () {
389 var $element = $(step.element);
397 if ((step.trigger === 'reload' || (step.trigger && step.trigger.url)) && _next) return;
399 if (step.snippet && step.trigger === 'drag') {
400 website.TestConsole.dragAndDropSnippet(step.snippet);
401 } else if (step.trigger && step.trigger.id === 'change') {
402 $element.trigger($.Event("change", { srcElement: $element }));
403 } else if (step.sampleText) {
404 $element.val(step.sampleText);
405 $element.trigger($.Event("change", { srcElement: $element }));
406 } else if ($element.is(":visible")) { // Click by default
407 if (step.trigger.id === 'mousedown') {
408 $element.trigger($.Event("mousedown", { srcElement: $element }));
410 var evt = document.createEvent("MouseEvents");
411 evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
412 $element[0].dispatchEvent(evt);
413 if (step.trigger.id === 'mouseup') {
414 $element.trigger($.Event("mouseup", { srcElement: $element }));
417 if (!step.triggers) next();
420 var url = new website.UrlParser(window.location.href);
421 if (tour.path && url.pathname !== tour.path && !window.localStorage.getItem(testId)) {
422 window.localStorage.setItem(testId, actionSteps[0].stepId);
423 window.location.href = tour.path;
425 var lastStepId = window.localStorage.getItem(testId);
426 var currentStep = actionSteps.shift();
428 while (currentStep && lastStepId !== currentStep.stepId) {
429 currentStep = actionSteps.shift();
432 if (currentStep.snippet && $(currentStep.element).length === 0) {
433 self.on('rte:ready', this, function () {
434 executeStep(currentStep);
437 executeStep(currentStep);
442 window.localStorage.removeItem(testId);
443 window.localStorage.removeItem("test-report");
444 for (var k in window.localStorage) {
445 if (window.localStorage[k].indexOf("test-last")) {
446 window.localStorage.removeItem(k);
451 website.TestConsole.tests.push(test);
452 if (window.localStorage.getItem(testId)) {