[IMP] Robustify automated tour execution
[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         tour: undefined,
9         steps: [],
10         tourStorage: window.localStorage,
11         init: function () {
12             this.tour = new Tour({
13                 name: this.id,
14                 storage: this.tourStorage,
15                 keyboard: false,
16                 template: this.popover(),
17                 onHide: function () {
18                     window.scrollTo(0, 0);
19                 }
20             });
21             this.registerSteps();
22         },
23         registerSteps: function () {
24             var self = this;
25             this.tour.addSteps(_.map(this.steps, function (step) {
26                 step.title = openerp.qweb.render('website.tour_popover_title', { title: step.title });
27                 if (!step.element) {
28                     step.orphan = true;
29                 }
30                 if (step.snippet) {
31                     step.element = '#oe_snippets div.oe_snippet[data-snippet-id="'+step.snippet+'"] .oe_snippet_thumbnail';
32                 }
33                 if (step.trigger) {
34                     if (step.trigger === 'click') {
35                         step.triggers = function (callback) {
36                             $(step.element).one('click', function () {
37                                 (callback || self.moveToNextStep).apply(self);
38                             });
39                         };
40                     } else if (step.trigger === 'drag') {
41                         step.triggers = function (callback) {
42                             self.onSnippetDragged(callback || self.moveToNextStep);
43                         };
44                     } else if (step.trigger && step.trigger.id) {
45                         if (step.trigger.emitter && step.trigger.type === 'openerp') {
46                             step.triggers = function (callback) {
47                                 step.trigger.emitter.on(step.trigger.id, self, function customHandler () {
48                                     step.trigger.emitter.off(step.trigger.id, customHandler);
49                                     (callback || self.moveToNextStep).apply(self, arguments);
50                                 });
51                             };
52                         } else {
53                             step.triggers = function (callback) {
54                                 var emitter = _.isString(step.trigger.emitter) ? $(step.trigger.emitter) : (step.trigger.emitter || $(step.element));
55                                 emitter.on(step.trigger.id, function () {
56                                     (callback || self.moveToNextStep).apply(self, arguments);
57                                 });
58                             };
59                         }
60                     } else if (step.trigger.modal) {
61                         step.triggers = function (callback) {
62                             var $doc = $(document);
63                             function onStop () {
64                                 if (step.trigger.modal.stopOnClose) {
65                                     self.stop();
66                                 }
67                             }
68                             $doc.on('hide.bs.modal', onStop);
69                             $doc.one('shown.bs.modal', function () {
70                                 $('.modal button.btn-primary').one('click', function () {
71                                     $doc.off('hide.bs.modal', onStop);
72                                     self.moveToStep(step.trigger.modal.afterSubmit);
73                                 });
74                                 (callback || self.moveToNextStep).apply(self);
75                             });
76                         };
77                     }
78                 }
79                 step.onShow = (function () {
80                     var executed = false;
81                     return function () {
82                         if (!executed) {
83                             _.isFunction(step.onStart) && step.onStart();
84                             _.isFunction(step.triggers) && step.triggers();
85                             executed = true;
86                         }
87                     };
88                 }());
89                 return step;
90             }));
91         },
92         reset: function () {
93             this.tourStorage.removeItem(this.id+'_current_step');
94             this.tourStorage.removeItem(this.id+'_end');
95             this.tour._current = 0;
96             $('.popover.tour').remove();
97         },
98         start: function () {
99             if (this.resume() || ((this.currentStepIndex() === 0) && !this.tour.ended())) {
100                 this.tour.start();
101             }
102         },
103         currentStepIndex: function () {
104             var index = this.tourStorage.getItem(this.id+'_current_step') || 0;
105             return parseInt(index, 10);
106         },
107         indexOfStep: function (stepId) {
108             var index = -1;
109             _.each(this.steps, function (step, i) {
110                if (step.stepId === stepId) {
111                    index = i;
112                }
113             });
114             return index;
115         },
116         isCurrentStep: function (stepId) {
117             return this.currentStepIndex() === this.indexOfStep(stepId);
118         },
119         moveToStep: function (step) {
120             var index = _.isNumber(step) ? step : this.indexOfStep(step);
121             if (index >= this.steps.length) {
122                 this.stop();
123             } else if (index >= 0) {
124                 var self = this;
125                 $('.popover.tour').remove();
126                 setTimeout(function () {
127                     setTimeout(function () {
128                         self.tour.goto(index);
129                     }, 0);
130                 }, 0);
131             }
132         },
133         moveToNextStep: function () {
134             var nextStepIndex = this.currentStepIndex() + 1;
135             this.moveToStep(nextStepIndex);
136         },
137         stop: function () {
138             this.tour.end();
139         },
140         redirect: function (url) {
141             url = url || new website.UrlParser(window.location.href);
142             var path = (this.startPath && url.pathname !== this.startPath) ? this.startPath : url.pathname;
143             var search = url.activateTutorial(this.id);
144             var newUrl = path + search;
145             window.location.replace(newUrl);
146         },
147         ended: function () {
148             return this.tourStorage.getItem(this.id+'_end') === "yes";
149         },
150         resume: function () {
151             // Override if necessary
152             return this.tourStorage.getItem(this.id+'_current_step') && !this.ended();
153         },
154         trigger: function (url) {
155             // Override if necessary
156             url = url || new website.UrlParser(window.location.href);
157             return url.isActive(this.id);
158         },
159         testUrl: function (pattern) {
160             var url = new website.UrlParser(window.location.href);
161             return pattern.test(url.pathname+url.search);
162         },
163         popover: function (options) {
164             return openerp.qweb.render('website.tour_popover', options);
165         },
166         onSnippetDragged: function (callback) {
167             var self = this;
168             function beginDrag () {
169                 $('.popover.tour').remove();
170                 function advance () {
171                     if (_.isFunction(callback)) {
172                         callback.apply(self);
173                     }
174                 }
175                 $(document.body).one('mouseup', advance);
176             }
177             $('#website-top-navbar [data-snippet-id].ui-draggable').one('mousedown', beginDrag);
178         },
179         onSnippetDraggedAdvance: function () {
180             onSnippetDragged(self.moveToNextStep);
181         },
182     });
183
184     website.UrlParser = openerp.Class.extend({
185         init: function (url) {
186             var a = document.createElement('a');
187             a.href = url;
188             this.href = a.href;
189             this.host = a.host;
190             this.protocol = a.protocol;
191             this.port = a.port;
192             this.hostname = a.hostname;
193             this.pathname = a.pathname;
194             this.origin = a.origin;
195             this.search = a.search;
196             this.hash = a.hash;
197             function generateTrigger (id) {
198                 return "tutorial."+id+"=true";
199             }
200             this.activateTutorial = function (id) {
201                 var urlTrigger = generateTrigger(id);
202                 var querystring = _.filter(this.search.split('?'), function (str) {
203                     return str;
204                 });
205                 if (querystring.length > 0) {
206                     var queries = _.filter(querystring[0].split("&"), function (query) {
207                         return query.indexOf("tutorial.") < 0
208                     });
209                     queries.push(urlTrigger);
210                     return "?"+_.uniq(queries).join("&");
211                 } else {
212                     return "?"+urlTrigger;
213                 }
214             };
215             this.isActive = function (id) {
216                 var urlTrigger = generateTrigger(id);
217                 return this.search.indexOf(urlTrigger) >= 0;
218             };
219         },
220     });
221
222     var TestConsole = openerp.Class.extend({
223         tests: [],
224         editor: null,
225         init: function (editor) {
226             if (!editor) {
227                 throw new Error("Editor cannot be null or undefined");
228             }
229             this.editor = editor;
230         },
231         test: function (id) {
232             return _.find(this.tests, function (tour) {
233                return tour.id === id;
234             });
235         },
236         snippetSelector: function (snippetId) {
237             return '#oe_snippets div.oe_snippet[data-snippet-id="'+snippetId+'"] .oe_snippet_thumbnail';
238         },
239         snippetThumbnail: function (snippetId) {
240             return $(this.snippetSelector(snippetId)).first();
241         },
242         snippetThumbnailExists: function (snippetId) {
243             return this.snippetThumbnail(snippetId).length > 0;
244         },
245         dragAndDropSnippet: function (snippetId) {
246             function actualDragAndDrop ($thumbnail) {
247                 var thumbnailPosition = $thumbnail.position();
248                 $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top }));
249                 $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top+500 }));
250                 var $dropZone = $(".oe_drop_zone").first();
251                 var dropPosition = $dropZone.position();
252                 $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
253             }
254             if (this.snippetThumbnailExists(snippetId)) {
255                 actualDragAndDrop(this.snippetThumbnail(snippetId));
256             } else {
257                 this.editor.on('rte:ready', this, function () {
258                     actualDragAndDrop(this.snippetThumbnail(snippetId));
259                 });
260             }
261         },
262     });
263
264     website.EditorBar.include({
265         tours: [],
266         init: function () {
267             var result = this._super();
268             website.TestConsole = new TestConsole(this);
269             return result;
270         },
271         start: function () {
272             $('.tour-backdrop').click(function (e) {
273                 e.stopImmediatePropagation();
274                 e.preventDefault();
275             });
276             var url = new website.UrlParser(window.location.href);
277             var menu = $('#help-menu');
278             _.each(this.tours, function (tour) {
279                 var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
280                 $menuItem.click(function () {
281                     tour.redirect(url);
282                     tour.reset();
283                     tour.start();
284                 });
285                 menu.append($menuItem);
286                 if (tour.trigger()) {
287                     tour.start();
288                 }
289             });
290             return this._super();
291         },
292         registerTour: function (tour) {
293             var testId = 'test_'+tour.id+'_tour';
294             this.tours.push(tour);
295             var test = {
296                 id: tour.id,
297                 run: function (force) {
298                     if (force === true) {
299                         this.reset();
300                     }
301                     var actionSteps = _.filter(tour.steps, function (step) {
302                        return step.trigger;
303                     });
304                     function executeStep (step) {
305                         window.localStorage.setItem(testId, step.stepId);
306                         step.triggers(function () {
307                             var nextStep = actionSteps.shift();
308                             if (nextStep) {
309                                 // Ensure the previous step has been fully propagated
310                                 setTimeout(function () {
311                                     setTimeout(function () {
312                                         executeStep(nextStep);
313                                     }, 0);
314                                 }, 0);
315                             } else {
316                                 window.localStorage.removeItem(testId);
317                             }
318                         });
319                         var $element = $(step.element);
320                         if (step.snippet && step.trigger === 'drag') {
321                             website.TestConsole.dragAndDropSnippet(step.snippet);
322                         } else if (step.trigger.id === 'change') {
323                             var currentValue = $element.val();
324                             var options = $element[0].options;
325                             // FIXME: It may be necessary to set a particular value
326                             var newValue = _.find(options, function (option) {
327                                 return option.value !== currentValue;
328                             }).value;
329                             $element.val(newValue).trigger($.Event("change"));
330                         } else {
331                             $element.trigger($.Event("click", { srcElement: $element }));
332                         }
333                     }
334                     var url = new website.UrlParser(window.location.href);
335                     if (tour.startPath && url.pathname !== tour.startPath) {
336                         window.localStorage.setItem(testId, actionSteps[0].stepId);
337                         window.location.href = tour.startPath;
338                     } else {
339                         var lastStepId = window.localStorage.getItem(testId);
340                         var currentStep = actionSteps.shift();
341                         if (lastStepId) {
342                             while (currentStep && lastStepId !== currentStep.stepId) {
343                                 currentStep = actionSteps.shift();
344                             }
345                         }
346                         setTimeout(function () {
347                             executeStep(currentStep);
348                         }, 0);
349                     }
350                 },
351                 reset: function () {
352                     window.localStorage.removeItem(testId);
353                 },
354             };
355             website.TestConsole.tests.push(test);
356             if (window.localStorage.getItem(testId)) {
357                 test.run();
358             }
359         },
360     });
361
362 }());