eef3d1b8ae7ef98b74ef275797ece08e61afac09
[odoo/odoo.git] / addons / website / static / src / js / website.tour.js
1 (function () {
2     'use strict';
3
4     var website = openerp.website;
5     website.add_template_file('/website/static/src/xml/website.tour.xml');
6
7     website.Tour = openerp.Class.extend({
8         steps: [], // Override
9         tourStorage: window.localStorage, // FIXME: will break on iPad in private mode
10         init: function () {
11             this.tour = new Tour({
12                 name: this.id,
13                 storage: this.tourStorage,
14                 keyboard: false,
15                 template: this.popover(),
16                 onHide: function () {
17                     window.scrollTo(0, 0);
18                 }
19             });
20             this.registerSteps();
21         },
22         registerStep: function (step) {
23             var self = this;
24             step.title = openerp.qweb.render('website.tour_popover_title', { title: step.title });
25             if (!step.element) {
26                 step.orphan = true;
27             }
28             if (step.snippet) {
29                 step.element = '#oe_snippets div.oe_snippet[data-snippet-id="'+step.snippet+'"] .oe_snippet_thumbnail';
30             }
31             if (step.trigger) {
32                 if (step.trigger === 'click') {
33                     step.triggers = function (callback) {
34                         $(step.element).one('click', function () {
35                             (callback || self.moveToNextStep).apply(self);
36                         });
37                     };
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);
42                         if (index !== -1) {
43                             stack.splice(index,1);
44                             (callback || self.moveToNextStep).apply(self);
45                         } else {
46                             stack.push(step.stepId);
47                         }
48                         localStorage.setItem("website-reloads", JSON.stringify(stack));
49                     };
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);
55                         if (index !== -1) {
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);
60                             if (!test) return;
61                             stack.splice(index,1);
62                             (callback || self.moveToNextStep).apply(self);
63                         } else {
64                             stack.push(id);
65                         }
66                         localStorage.setItem("website-geturls", JSON.stringify(stack));
67                     };
68                 } else if (step.trigger === 'drag') {
69                     step.triggers = function (callback) {
70                         self.onSnippetDragged(callback || self.moveToNextStep);
71                     };
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);
78                             });
79                         };
80                     } else {
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);
86                             });
87                         };
88                     }
89                 } else if (step.trigger.modal) {
90                     step.triggers = function (callback) {
91                         var $doc = $(document);
92                         function onStop () {
93                             if (step.trigger.modal.stopOnClose) {
94                                 self.stop();
95                             }
96                         }
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]);
102                             });
103                             (callback || self.moveToNextStep).apply(self);
104                         });
105                     };
106                 }
107             }
108             step.onShow = (function () {
109                 var executed = false;
110                 return function () {
111                     if (!executed) {
112                         _.isFunction(step.onStart) && step.onStart();
113                         _.isFunction(step.triggers) && step.triggers();
114                         executed = true;
115                     }
116                 };
117             }());
118             return step;
119         },
120         registerSteps: function () {
121             var self = this;
122             this.tour.addSteps(_.map(this.steps, function (step) {
123                 return self.registerStep(step);
124             }));
125         },
126         reset: function () {
127             this.tourStorage.removeItem(this.id+'_current_step');
128             this.tourStorage.removeItem(this.id+'_end');
129             this.tour._current = 0;
130             $('.popover.tour').remove();
131         },
132         start: function () {
133             if (this.resume() || ((this.currentStepIndex() === 0) && !this.tour.ended())) {
134                 this.tour.start();
135             }
136         },
137         currentStepIndex: function () {
138             var index = this.tourStorage.getItem(this.id+'_current_step') || 0;
139             return parseInt(index, 10);
140         },
141         indexOfStep: function (stepId) {
142             var index = -1;
143             _.each(this.steps, function (step, i) {
144                if (step.stepId === stepId) {
145                    index = i;
146                }
147             });
148             return index;
149         },
150         isCurrentStep: function (stepId) {
151             return this.currentStepIndex() === this.indexOfStep(stepId);
152         },
153         moveToStep: function (step) {
154             var index = _.isNumber(step) ? step : this.indexOfStep(step);
155             if (index >= this.steps.length) {
156                 this.stop();
157             } else if (index >= 0) {
158                 var self = this;
159                 $('.popover.tour').remove();
160                 setTimeout(function () {
161                     setTimeout(function () {
162                         self.tour.goto(index);
163                     }, 0);
164                 }, 0);
165             }
166         },
167         moveToNextStep: function () {
168             var nextStepIndex = this.currentStepIndex() + 1;
169             this.moveToStep(nextStepIndex);
170         },
171         stop: function () {
172             this.tour.end();
173         },
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);
180         },
181         ended: function () {
182             return this.tourStorage.getItem(this.id+'_end') === "yes";
183         },
184         resume: function () {
185             // Override if necessary
186             return this.tourStorage.getItem(this.id+'_current_step') && !this.ended();
187         },
188         trigger: function (url) {
189             // Override if necessary
190             url = url || new website.UrlParser(window.location.href);
191             return url.isActive(this.id);
192         },
193         testUrl: function (pattern) {
194             var url = new website.UrlParser(window.location.href);
195             return pattern.test(url.pathname+url.search);
196         },
197         popover: function (options) {
198             return openerp.qweb.render('website.tour_popover', options);
199         },
200         onSnippetDragged: function (callback) {
201             var self = this;
202             function beginDrag () {
203                 $('.popover.tour').remove();
204                 function advance () {
205                     if (_.isFunction(callback)) {
206                         callback.apply(self);
207                     }
208                 }
209                 $(document.body).one('mouseup', advance);
210             }
211             $('#website-top-navbar [data-snippet-id].ui-draggable').one('mousedown', beginDrag);
212         },
213         onSnippetDraggedAdvance: function () {
214             onSnippetDragged(self.moveToNextStep);
215         },
216     });
217
218     website.UrlParser = openerp.Class.extend({
219         init: function (url) {
220             var a = document.createElement('a');
221             a.href = url;
222             this.href = a.href;
223             this.host = a.host;
224             this.protocol = a.protocol;
225             this.port = a.port;
226             this.hostname = a.hostname;
227             this.pathname = a.pathname;
228             this.origin = a.origin;
229             this.search = a.search;
230             this.hash = a.hash;
231             function generateTrigger (id) {
232                 return "tutorial."+id+"=true";
233             }
234             this.activateTutorial = function (id) {
235                 var urlTrigger = generateTrigger(id);
236                 var querystring = _.filter(this.search.split('?'), function (str) {
237                     return str;
238                 });
239                 if (querystring.length > 0) {
240                     var queries = _.filter(querystring[0].split("&"), function (query) {
241                         return query.indexOf("tutorial.") < 0
242                     });
243                     queries.push(urlTrigger);
244                     return "?"+_.uniq(queries).join("&");
245                 } else {
246                     return "?"+urlTrigger;
247                 }
248             };
249             this.isActive = function (id) {
250                 var urlTrigger = generateTrigger(id);
251                 return this.search.indexOf(urlTrigger) >= 0;
252             };
253         },
254     });
255
256     var TestConsole = openerp.Class.extend({
257         tests: [],
258         editor: null,
259         init: function (editor) {
260             if (!editor) {
261                 throw new Error("Editor cannot be null or undefined");
262             }
263             this.editor = editor;
264         },
265         test: function (id) {
266             return _.find(this.tests, function (tour) {
267                return tour.id === id;
268             });
269         },
270         snippetSelector: function (snippetId) {
271             return '#oe_snippets div.oe_snippet[data-snippet-id="'+snippetId+'"] .oe_snippet_thumbnail';
272         },
273         snippetThumbnail: function (snippetId) {
274             return $(this.snippetSelector(snippetId)).first();
275         },
276         snippetThumbnailExists: function (snippetId) {
277             return this.snippetThumbnail(snippetId).length > 0;
278         },
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 }));
287             }
288             if (this.snippetThumbnailExists(snippetId)) {
289                 actualDragAndDrop(this.snippetThumbnail(snippetId));
290             } else {
291                 this.editor.on('rte:ready', this, function () {
292                     actualDragAndDrop(this.snippetThumbnail(snippetId));
293                 });
294             }
295         },
296     });
297
298     website.EditorBar.include({
299         tours: [],
300         init: function () {
301             var result = this._super();
302             website.TestConsole = new TestConsole(this);
303             return result;
304         },
305         start: function () {
306             $('.tour-backdrop').click(function (e) {
307                 e.stopImmediatePropagation();
308                 e.preventDefault();
309             });
310             var menu = $('#help-menu');
311             _.each(this.tours, function (tour) {
312                 var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
313                 $menuItem.click(function () {
314                     tour.redirect(new website.UrlParser(window.location.href));
315                     tour.reset();
316                     tour.start();
317                 });
318                 menu.append($menuItem);
319                 if (tour.trigger()) {
320                     tour.start();
321                 }
322             });
323             return this._super();
324         },
325         registerTour: function (tour) {
326             var self = this;
327             var testId = 'test_'+tour.id+'_tour';
328             this.tours.push(tour);
329             var defaultDelay = 500; //ms
330             var overlapsCrash;
331             var test = {
332                 id: tour.id,
333                 run: function (force) {
334                     if (force === true) {
335                         this.reset();
336                     }
337                     var actionSteps = _.filter(tour.steps, function (step) {
338                        return step.trigger || step.sampleText;
339                     });
340                     window.onbeforeunload = function () {
341                         clearTimeout(overlapsCrash);
342                     };
343                     function executeStep (step) {
344                         var lastStep = window.localStorage.getItem(testId);
345                         var tryStep = lastStep != step.stepId ? 0 : parseInt(window.localStorage.getItem("last-"+testId) || 0, 10)+1;
346                         window.localStorage.setItem("last-"+testId, tryStep);
347                         if (tryStep > 2) {
348                             window.localStorage.removeItem(testId);
349                             throw "Test: '" + testId + "' cycling stape: '" + step.stepId + "'";
350                         }
351
352                         var _next = false;
353                         window.localStorage.setItem(testId, step.stepId);
354                         function next () {
355                             clearTimeout(overlapsCrash);
356                             _next = true;
357                             var nextStep = actionSteps.shift();
358                             if (nextStep) {
359                                 setTimeout(function () {
360                                     executeStep(nextStep);
361                                 }, step.delay || defaultDelay);
362                             } else {
363                                 window.localStorage.removeItem(testId);
364                             }
365                         }
366                         overlapsCrash = setTimeout(function () {
367                             window.localStorage.removeItem(testId);
368                             throw "Test: '" + testId + "' can't resolve stape: '" + step.stepId + "'";
369                         }, (step.delay || defaultDelay) + 500);
370
371                         var $element = $(step.element);
372                         if (step.triggers) step.triggers(next);
373                         if ((step.trigger === 'reload' || step.trigger.url) && _next) return;
374                         
375                         if (step.snippet && step.trigger === 'drag') {
376                             website.TestConsole.dragAndDropSnippet(step.snippet);
377                         } else if (step.trigger && step.trigger.id === 'change') {
378                             $element.trigger($.Event("change", { srcElement: $element }));
379                         } else if (step.sampleText) {
380                             $element.val(step.sampleText);
381                             $element.trigger($.Event("change", { srcElement: $element }));
382                         } else if ($element.is(":visible")) { // Click by default
383                             if (step.trigger.id === 'mousedown') {
384                                 $element.trigger($.Event("mousedown", { srcElement: $element }));
385                             }
386                             var evt = document.createEvent("MouseEvents");
387                             evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
388                             $element[0].dispatchEvent(evt);
389                             if (step.trigger.id === 'mouseup') {
390                                 $element.trigger($.Event("mouseup", { srcElement: $element }));
391                             }
392                         }
393                         if (!step.triggers) next();
394                     }
395                     var url = new website.UrlParser(window.location.href);
396                     if (tour.path && url.pathname !== tour.path && !window.localStorage.getItem(testId)) {
397                         window.localStorage.setItem(testId, actionSteps[0].stepId);
398                         window.location.href = tour.path;
399                     } else {
400                         var lastStepId = window.localStorage.getItem(testId);
401                         var currentStep = actionSteps.shift();
402                         if (lastStepId) {
403                             while (currentStep && lastStepId !== currentStep.stepId) {
404                                 currentStep = actionSteps.shift();
405                             }
406                         }
407                         if (currentStep.snippet && $(currentStep.element).length === 0) {
408                             self.on('rte:ready', this, function () {
409                                 executeStep(currentStep);
410                             });
411                         } else {
412                             executeStep(currentStep);
413                         }
414                     }
415                 },
416                 reset: function () {
417                     window.localStorage.removeItem(testId);
418                 },
419             };
420             website.TestConsole.tests.push(test);
421             if (window.localStorage.getItem(testId)) {
422                 test.run();
423             }
424         },
425     });
426
427 }());