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));
298 website.EditorBar.include({
301 var result = this._super();
302 website.TestConsole = new TestConsole(this);
306 $('.tour-backdrop').click(function (e) {
307 e.stopImmediatePropagation();
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));
318 menu.append($menuItem);
319 if (tour.trigger()) {
323 return this._super();
325 registerTour: function (tour) {
327 var testId = 'test_'+tour.id+'_tour';
328 this.tours.push(tour);
329 var defaultDelay = 500; //ms
333 run: function (force) {
334 if (force === true) {
337 var actionSteps = _.filter(tour.steps, function (step) {
338 return step.trigger || step.sampleText;
340 window.onbeforeunload = function () {
341 clearTimeout(overlapsCrash);
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);
348 window.localStorage.removeItem(testId);
349 throw "Test: '" + testId + "' cycling step: '" + step.stepId + "'";
353 window.localStorage.setItem(testId, step.stepId);
355 clearTimeout(overlapsCrash);
357 var nextStep = actionSteps.shift();
359 setTimeout(function () {
360 executeStep(nextStep);
361 }, step.delay || defaultDelay);
363 window.localStorage.removeItem(testId);
366 overlapsCrash = setTimeout(function () {
367 window.localStorage.removeItem(testId);
368 throw "Test: '" + testId + "' can't resolve step: '" + step.stepId + "'";
369 }, (step.delay || defaultDelay) + 500);
371 var $element = $(step.element);
372 if (step.triggers) step.triggers(next);
373 if ((step.trigger === 'reload' || (step.trigger && step.trigger.url)) && _next) return;
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 }));
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 }));
393 if (!step.triggers) next();
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;
400 var lastStepId = window.localStorage.getItem(testId);
401 var currentStep = actionSteps.shift();
403 while (currentStep && lastStepId !== currentStep.stepId) {
404 currentStep = actionSteps.shift();
407 if (currentStep.snippet && $(currentStep.element).length === 0) {
408 self.on('rte:ready', this, function () {
409 executeStep(currentStep);
412 executeStep(currentStep);
417 window.localStorage.removeItem(testId);
420 website.TestConsole.tests.push(test);
421 if (window.localStorage.getItem(testId)) {