roroacms 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. data/MIT-LICENSE +20 -20
  2. data/README.md +62 -61
  3. data/Rakefile +19 -19
  4. data/app/assets/javascripts/roroacms/admin/application.js +1 -3
  5. data/app/assets/javascripts/roroacms/admin/roroacms-tour.js +46 -46
  6. data/app/assets/javascripts/roroacms/application.js +9 -21
  7. data/app/assets/javascripts/roroacms/vendor/hopscotch.js +2409 -2409
  8. data/app/assets/stylesheets/roroacms/admin/application.css +5 -3
  9. data/app/assets/stylesheets/roroacms/vendor/hopscotch.css +512 -512
  10. data/app/assets/stylesheets/roroacms/vendor/jquery/jquery-ui.css +1177 -1177
  11. data/app/controllers/roroacms/admin_controller.rb +1 -0
  12. data/app/controllers/roroacms/application_controller.rb +5 -3
  13. data/app/helpers/roroacms/admin_roroa_helper.rb +4 -4
  14. data/app/helpers/roroacms/admin_view_helper.rb +16 -16
  15. data/app/helpers/roroacms/application_helper.rb +4 -4
  16. data/app/helpers/roroacms/general_helper.rb +8 -8
  17. data/app/helpers/roroacms/new_view_helper.rb +7 -6
  18. data/app/helpers/roroacms/routing_helper.rb +15 -15
  19. data/app/helpers/roroacms/seo_helper.rb +2 -1
  20. data/app/helpers/roroacms/theme_helper.rb +6 -6
  21. data/app/helpers/roroacms/view_helper.rb +4 -4
  22. data/app/mailers/roroacms/emailer.rb +31 -31
  23. data/app/views/layouts/roroacms/application.html.erb +1 -1
  24. data/app/views/roroacms/admin/partials/_append_sidebar_menu.html.erb +51 -51
  25. data/app/views/roroacms/admin/partials/_comment.html.erb +10 -1
  26. data/app/views/roroacms/admin/partials/_editor.html.erb +21 -21
  27. data/app/views/roroacms/setup/administrator.html.erb +72 -72
  28. data/app/views/roroacms/setup/index.html.erb +108 -108
  29. data/config/initializers/devise.rb +258 -258
  30. data/config/locales/devise.en.yml +115 -115
  31. data/config/locales/en.yml +1 -1
  32. data/config/routes.rb +96 -96
  33. data/db/migrate/20140801203556_add_admins.rb +44 -44
  34. data/db/migrate/20140802084604_add_roroacms_comments.rb +31 -31
  35. data/db/migrate/20140802084615_add_roroacms_menu_options.rb +24 -24
  36. data/db/migrate/20140802084624_add_roroacms_menus.rb +18 -18
  37. data/db/migrate/20140802084631_add_roroacms_posts.rb +42 -42
  38. data/db/migrate/20140802084638_add_roroacms_settings.rb +21 -21
  39. data/db/migrate/20140802084658_add_roroacms_term_anatomies.rb +20 -20
  40. data/db/migrate/20140802084707_add_roroacms_term_relationships.rb +23 -23
  41. data/db/migrate/20140802084717_add_roroacms_terms.rb +27 -27
  42. data/lib/ext/string.rb +4 -4
  43. data/lib/generators/roroacms/install_generator.rb +16 -16
  44. data/lib/roroacms/engine.rb +4 -5
  45. data/lib/roroacms/version.rb +3 -3
  46. data/lib/roroacms.rb +5 -5
  47. data/lib/tasks/roroacms_tasks.rake +9 -9
  48. data/spec/controllers/roroacms/admin/themes_controller_spec.rb +2 -2
  49. data/spec/dummy/README.rdoc +28 -28
  50. data/spec/dummy/Rakefile +6 -6
  51. data/spec/dummy/app/controllers/application_controller.rb +5 -5
  52. data/spec/dummy/app/helpers/application_helper.rb +2 -2
  53. data/spec/dummy/app/helpers/theme_helper.rb +12 -12
  54. data/spec/dummy/app/views/layouts/application.html.erb +14 -14
  55. data/spec/dummy/app/views/theme/roroa-bootstrap-3/theme_helper.rb +8 -8
  56. data/spec/dummy/app/views/theme/roroa1/assets/stylesheets/style.css.scss +8 -8
  57. data/spec/dummy/app/views/theme/roroa1/layout.html.erb +1 -1
  58. data/spec/dummy/bin/bundle +3 -3
  59. data/spec/dummy/bin/rails +4 -4
  60. data/spec/dummy/bin/rake +4 -4
  61. data/spec/dummy/config/application.rb +23 -23
  62. data/spec/dummy/config/boot.rb +5 -5
  63. data/spec/dummy/config/database.yml +7 -7
  64. data/spec/dummy/config/environment.rb +5 -5
  65. data/spec/dummy/config/environments/development.rb +37 -37
  66. data/spec/dummy/config/environments/production.rb +82 -82
  67. data/spec/dummy/config/environments/test.rb +39 -39
  68. data/spec/dummy/config/initializers/assets.rb +8 -8
  69. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -7
  70. data/spec/dummy/config/initializers/cookies_serializer.rb +2 -2
  71. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -4
  72. data/spec/dummy/config/initializers/inflections.rb +16 -16
  73. data/spec/dummy/config/initializers/mime_types.rb +4 -4
  74. data/spec/dummy/config/initializers/session_store.rb +3 -3
  75. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -14
  76. data/spec/dummy/config/locales/en.yml +23 -23
  77. data/spec/dummy/config/routes.rb +3 -3
  78. data/spec/dummy/config/secrets.yml +22 -22
  79. data/spec/dummy/config.ru +4 -4
  80. data/spec/dummy/db/schema.rb +149 -149
  81. data/spec/dummy/public/404.html +67 -67
  82. data/spec/dummy/public/422.html +67 -67
  83. data/spec/dummy/public/500.html +66 -66
  84. data/spec/dummy/public/javascripts/translations.js +1 -1
  85. data/spec/dummy/tmp/cache/i18n-js.yml +36 -36
  86. data/spec/models/roroacms/admin_spec.rb +67 -67
  87. data/spec/spec_helper.rb +35 -35
  88. data/spec/support/factories.rb +79 -79
  89. metadata +2 -2
@@ -1,2409 +1,2409 @@
1
- (function(context, namespace) {
2
- var Hopscotch,
3
- HopscotchBubble,
4
- HopscotchCalloutManager,
5
- HopscotchI18N,
6
- customI18N,
7
- customRenderer,
8
- customEscape,
9
- templateToUse = 'bubble_default',
10
- Sizzle = window.Sizzle || null,
11
- utils,
12
- callbacks,
13
- helpers,
14
- winLoadHandler,
15
- defaultOpts,
16
- winHopscotch = context[namespace],
17
- undefinedStr = 'undefined',
18
- waitingToStart = false, // is a tour waiting for the document to finish
19
- // loading so that it can start?
20
- hasJquery = (typeof window.jQuery !== undefinedStr),
21
- hasSessionStorage = false,
22
- document = window.document;
23
-
24
- // If cookies are disabled, accessing sessionStorage can throw an error.
25
- try {
26
- hasSessionStorage = (typeof window.sessionStorage !== undefinedStr);
27
- } catch (err) {}
28
-
29
- defaultOpts = {
30
- smoothScroll: true,
31
- scrollDuration: 1000,
32
- scrollTopMargin: 200,
33
- showCloseButton: true,
34
- showPrevButton: false,
35
- showNextButton: true,
36
- bubbleWidth: 280,
37
- bubblePadding: 15,
38
- arrowWidth: 20,
39
- skipIfNoElement: true,
40
- cookieName: 'hopscotch.tour.state',
41
- highlight: false,
42
- highlightMargin: 0
43
- };
44
-
45
- if (winHopscotch) {
46
- // Hopscotch already exists.
47
- return;
48
- }
49
-
50
- if (!Array.isArray) {
51
- Array.isArray = function(obj) {
52
- return Object.prototype.toString.call(obj) === '[object Array]';
53
- };
54
- }
55
-
56
- /**
57
- * Called when the page is done loading.
58
- *
59
- * @private
60
- */
61
- winLoadHandler = function() {
62
- if (waitingToStart) {
63
- winHopscotch.startTour();
64
- }
65
- };
66
-
67
- /**
68
- * utils
69
- * =====
70
- * A set of utility functions, mostly for standardizing to manipulate
71
- * and extract information from the DOM. Basically these are things I
72
- * would normally use jQuery for, but I don't want to require it for
73
- * this framework.
74
- *
75
- * @private
76
- */
77
- utils = {
78
- /**
79
- * addClass
80
- * ========
81
- * Adds one or more classes to a DOM element.
82
- *
83
- * @private
84
- */
85
- addClass: function(domEl, classToAdd) {
86
- var domClasses,
87
- classToAddArr,
88
- setClass,
89
- i,
90
- len;
91
-
92
- if (!domEl.className) {
93
- domEl.className = classToAdd;
94
- }
95
- else {
96
- classToAddArr = classToAdd.split(/\s+/);
97
- domClasses = ' ' + domEl.className + ' ';
98
- for (i = 0, len = classToAddArr.length; i < len; ++i) {
99
- if (domClasses.indexOf(' ' + classToAddArr[i] + ' ') < 0) {
100
- domClasses += classToAddArr[i] + ' ';
101
- }
102
- }
103
- domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
104
- }
105
- },
106
-
107
- /**
108
- * removeClass
109
- * ===========
110
- * Remove one or more classes from a DOM element.
111
- *
112
- * @private
113
- */
114
- removeClass: function(domEl, classToRemove) {
115
- var domClasses,
116
- classToRemoveArr,
117
- currClass,
118
- i,
119
- len;
120
-
121
- classToRemoveArr = classToRemove.split(/\s+/);
122
- domClasses = ' ' + domEl.className + ' ';
123
- for (i = 0, len = classToRemoveArr.length; i < len; ++i) {
124
- domClasses = domClasses.replace(' ' + classToRemoveArr[i] + ' ', ' ');
125
- }
126
- domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
127
- },
128
-
129
- /**
130
- * hasClass
131
- * ========
132
- * Determine if a given DOM element has a class.
133
- */
134
- hasClass: function(domEl, classToCheck){
135
- var classes;
136
-
137
- if(!domEl.className){ return false; }
138
- classes = ' ' + domEl.className + ' ';
139
- return (classes.indexOf(' ' + classToCheck + ' ') !== -1);
140
- },
141
-
142
- /**
143
- * @private
144
- */
145
- getPixelValue: function(val) {
146
- var valType = typeof val;
147
- if (valType === 'number') { return val; }
148
- if (valType === 'string') { return parseInt(val, 10); }
149
- return 0;
150
- },
151
-
152
- /**
153
- * Inspired by Python... returns val if it's defined, otherwise returns the default.
154
- *
155
- * @private
156
- */
157
- valOrDefault: function(val, valDefault) {
158
- return typeof val !== undefinedStr ? val : valDefault;
159
- },
160
-
161
- /**
162
- * Invokes a single callback represented by an array.
163
- * Example input: ["my_fn", "arg1", 2, "arg3"]
164
- * @private
165
- */
166
- invokeCallbackArrayHelper: function(arr) {
167
- // Logic for a single callback
168
- var fn;
169
- if (Array.isArray(arr)) {
170
- fn = helpers[arr[0]];
171
- if (typeof fn === 'function') {
172
- return fn.apply(this, arr.slice(1));
173
- }
174
- }
175
- },
176
-
177
- /**
178
- * Invokes one or more callbacks. Array should have at most one level of nesting.
179
- * Example input:
180
- * ["my_fn", "arg1", 2, "arg3"]
181
- * [["my_fn_1", "arg1", "arg2"], ["my_fn_2", "arg2-1", "arg2-2"]]
182
- * [["my_fn_1", "arg1", "arg2"], function() { ... }]
183
- * @private
184
- */
185
- invokeCallbackArray: function(arr) {
186
- var i, len;
187
-
188
- if (Array.isArray(arr)) {
189
- if (typeof arr[0] === 'string') {
190
- // Assume there are no nested arrays. This is the one and only callback.
191
- return utils.invokeCallbackArrayHelper(arr);
192
- }
193
- else { // assume an array
194
- for (i = 0, len = arr.length; i < len; ++i) {
195
- utils.invokeCallback(arr[i]);
196
- }
197
- }
198
- }
199
- },
200
-
201
- /**
202
- * Helper function for invoking a callback, whether defined as a function literal
203
- * or an array that references a registered helper function.
204
- * @private
205
- */
206
- invokeCallback: function(cb) {
207
- if (typeof cb === 'function') {
208
- return cb();
209
- }
210
- if (typeof cb === 'string' && helpers[cb]) { // name of a helper
211
- return helpers[cb]();
212
- }
213
- else { // assuming array
214
- return utils.invokeCallbackArray(cb);
215
- }
216
- },
217
-
218
- /**
219
- * If stepCb (the step-specific helper callback) is passed in, then invoke
220
- * it first. Then invoke tour-wide helper.
221
- *
222
- * @private
223
- */
224
- invokeEventCallbacks: function(evtType, stepCb) {
225
- var cbArr = callbacks[evtType],
226
- callback,
227
- fn,
228
- i,
229
- len;
230
-
231
- if (stepCb) {
232
- return this.invokeCallback(stepCb);
233
- }
234
-
235
- for (i=0, len=cbArr.length; i<len; ++i) {
236
- this.invokeCallback(cbArr[i].cb);
237
- }
238
- },
239
-
240
- /**
241
- * @private
242
- */
243
- getScrollTop: function() {
244
- var scrollTop;
245
- if (typeof window.pageYOffset !== undefinedStr) {
246
- scrollTop = window.pageYOffset;
247
- }
248
- else {
249
- // Most likely IE <=8, which doesn't support pageYOffset
250
- scrollTop = document.documentElement.scrollTop;
251
- }
252
- return scrollTop;
253
- },
254
-
255
- /**
256
- * @private
257
- */
258
- getScrollLeft: function() {
259
- var scrollLeft;
260
- if (typeof window.pageXOffset !== undefinedStr) {
261
- scrollLeft = window.pageXOffset;
262
- }
263
- else {
264
- // Most likely IE <=8, which doesn't support pageXOffset
265
- scrollLeft = document.documentElement.scrollLeft;
266
- }
267
- return scrollLeft;
268
- },
269
-
270
- /**
271
- * @private
272
- */
273
- getWindowHeight: function() {
274
- return window.innerHeight || document.documentElement.clientHeight;
275
- },
276
-
277
- /**
278
- * @private
279
- */
280
- getWindowWidth: function() {
281
- return window.innerWidth || document.documentElement.clientWidth;
282
- },
283
-
284
- /**
285
- * @private
286
- */
287
- addEvtListener: function(el, evtName, fn) {
288
- return el.addEventListener ? el.addEventListener(evtName, fn, false) : el.attachEvent('on' + evtName, fn);
289
- },
290
-
291
- /**
292
- * @private
293
- */
294
- removeEvtListener: function(el, evtName, fn) {
295
- return el.removeEventListener ? el.removeEventListener(evtName, fn, false) : el.detachEvent('on' + evtName, fn);
296
- },
297
-
298
- documentIsReady: function() {
299
- return document.readyState === 'complete' || document.readyState === 'interactive';
300
- },
301
-
302
- /**
303
- * @private
304
- */
305
- evtPreventDefault: function(evt) {
306
- if (evt.preventDefault) {
307
- evt.preventDefault();
308
- }
309
- else if (event) {
310
- event.returnValue = false;
311
- }
312
- },
313
-
314
- /**
315
- * @private
316
- */
317
- extend: function(obj1, obj2) {
318
- var prop;
319
- for (prop in obj2) {
320
- if (obj2.hasOwnProperty(prop)) {
321
- obj1[prop] = obj2[prop];
322
- }
323
- }
324
- },
325
-
326
- /**
327
- * Helper function to get a single target DOM element. We will try to
328
- * locate the DOM element through several ways, in the following order:
329
- *
330
- * 1) Passing the string into document.querySelector
331
- * 2) Passing the string to jQuery, if it exists
332
- * 3) Passing the string to Sizzle, if it exists
333
- * 4) Calling document.getElementById if it is a plain id
334
- *
335
- * Default case is to assume the string is a plain id and call
336
- * document.getElementById on it.
337
- *
338
- * @private
339
- */
340
- getStepTargetHelper: function(target){
341
- var result = document.getElementById(target);
342
-
343
- //Backwards compatibility: assume the string is an id
344
- if (result) {
345
- return result;
346
- }
347
- if (document.querySelector) {
348
- return document.querySelector(target);
349
- }
350
- if (hasJquery) {
351
- result = jQuery(target);
352
- return result.length ? result[0] : null;
353
- }
354
- if (Sizzle) {
355
- result = new Sizzle(target);
356
- return result.length ? result[0] : null;
357
- }
358
- // Regex test for id. Following the HTML 4 spec for valid id formats.
359
- // (http://www.w3.org/TR/html4/types.html#type-id)
360
- if (/^#[a-zA-Z][\w-_:.]*$/.test(target)) {
361
- return document.getElementById(target.substring(1));
362
- }
363
-
364
- return null;
365
- },
366
-
367
- /**
368
- * Given a step, returns the target DOM element associated with it. It is
369
- * recommended to only assign one target per step. However, there are
370
- * some use cases which require multiple step targets to be supplied. In
371
- * this event, we will use the first target in the array that we can
372
- * locate on the page. See the comments for getStepTargetHelper for more
373
- * information.
374
- *
375
- * @private
376
- */
377
- getStepTarget: function(step) {
378
- var queriedTarget;
379
-
380
- if (!step || !step.target) {
381
- return null;
382
- }
383
-
384
- if (typeof step.target === 'string') {
385
- //Just one target to test. Check, cache, and return its results.
386
- step.target = utils.getStepTargetHelper(step.target);
387
- return step.target;
388
- }
389
- else if (Array.isArray(step.target)) {
390
- // Multiple items to check. Check each and return the first success.
391
- // Assuming they are all strings.
392
- var i,
393
- len;
394
-
395
- for (i = 0, len = step.target.length; i < len; i++){
396
- if (typeof step.target[i] === 'string') {
397
- queriedTarget = utils.getStepTargetHelper(step.target[i]);
398
-
399
- if (queriedTarget) {
400
- // Replace step.target with result so we don't have to look it up again.
401
- step.target = queriedTarget;
402
- return queriedTarget;
403
- }
404
- }
405
- }
406
- return null;
407
- }
408
-
409
- // Assume that the step.target is a DOM element
410
- return step.target;
411
- },
412
-
413
- /**
414
- * Convenience method for getting an i18n string. Returns custom i18n value
415
- * or the default i18n value if no custom value exists.
416
- *
417
- * @private
418
- */
419
- getI18NString: function(key) {
420
- return customI18N[key] || HopscotchI18N[key];
421
- },
422
-
423
- // Tour session persistence for multi-page tours. Uses HTML5 sessionStorage if available, then
424
- // falls back to using cookies.
425
- //
426
- // The following cookie-related logic is borrowed from:
427
- // http://www.quirksmode.org/js/cookies.html
428
-
429
- /**
430
- * @private
431
- */
432
- setState: function(name,value,days) {
433
- var expires = '',
434
- date;
435
-
436
- if (hasSessionStorage) {
437
- sessionStorage.setItem(name, value);
438
- }
439
- else {
440
- if (days) {
441
- date = new Date();
442
- date.setTime(date.getTime()+(days*24*60*60*1000));
443
- expires = '; expires='+date.toGMTString();
444
- }
445
- document.cookie = name+'='+value+expires+'; path=/';
446
- }
447
- },
448
-
449
- /**
450
- * @private
451
- */
452
- getState: function(name) {
453
- var nameEQ = name + '=',
454
- ca = document.cookie.split(';'),
455
- i,
456
- c,
457
- state;
458
-
459
- if (hasSessionStorage) {
460
- state = sessionStorage.getItem(name);
461
- }
462
- else {
463
- for(i=0;i < ca.length;i++) {
464
- c = ca[i];
465
- while (c.charAt(0)===' ') {c = c.substring(1,c.length);}
466
- if (c.indexOf(nameEQ) === 0) {
467
- state = c.substring(nameEQ.length,c.length);
468
- break;
469
- }
470
- }
471
- }
472
-
473
- return state;
474
- },
475
-
476
- /**
477
- * @private
478
- */
479
- clearState: function(name) {
480
- if (hasSessionStorage) {
481
- sessionStorage.removeItem(name);
482
- }
483
- else {
484
- this.setState(name,'',-1);
485
- }
486
- }
487
- };
488
-
489
- utils.addEvtListener(window, 'load', winLoadHandler);
490
-
491
- callbacks = {
492
- next: [],
493
- prev: [],
494
- start: [],
495
- end: [],
496
- show: [],
497
- error: [],
498
- close: []
499
- };
500
-
501
- /**
502
- * helpers
503
- * =======
504
- * A map of functions to be used as callback listeners. Functions are
505
- * added to and removed from the map using the functions
506
- * Hopscotch.registerHelper() and Hopscotch.unregisterHelper().
507
- */
508
- helpers = {};
509
-
510
- HopscotchI18N = {
511
- stepNums: null,
512
- nextBtn: 'Next',
513
- prevBtn: 'Back',
514
- doneBtn: 'Done',
515
- skipBtn: 'Skip',
516
- closeTooltip: 'Close'
517
- };
518
-
519
- customI18N = {}; // Developer's custom i18n strings goes here.
520
-
521
- /**
522
- * HopscotchBubble
523
- *
524
- * @class The HopscotchBubble class represents the view of a bubble. This class is also used for Hopscotch callouts.
525
- */
526
- HopscotchBubble = function(opt) {
527
- this.init(opt);
528
- };
529
-
530
- HopscotchBubble.prototype = {
531
- isShowing: false,
532
-
533
- currStep: undefined,
534
-
535
- /**
536
- * setPosition
537
- *
538
- * Sets the position of the bubble using the bounding rectangle of the
539
- * target element and the orientation and offset information specified by
540
- * the JSON.
541
- */
542
- setPosition: function(step) {
543
- var bubbleBoundingHeight,
544
- bubbleBoundingWidth,
545
- boundingRect,
546
- top,
547
- left,
548
- arrowOffset,
549
- self = this,
550
- targetEl = utils.getStepTarget(step),
551
- el = this.element,
552
- arrowEl = this.arrowEl;
553
-
554
- bubbleBoundingWidth = el.offsetWidth;
555
- bubbleBoundingHeight = el.offsetHeight;
556
- utils.removeClass(el, 'fade-in-down fade-in-up fade-in-left fade-in-right');
557
-
558
- // Originally called it orientation, but placement is more intuitive.
559
- // Allowing both for now for backwards compatibility.
560
- if (!step.placement && step.orientation) {
561
- step.placement = step.orientation;
562
- }
563
-
564
- // SET POSITION
565
- boundingRect = targetEl.getBoundingClientRect();
566
- if (step.placement === 'top') {
567
- top = (boundingRect.top - bubbleBoundingHeight) - this.opt.arrowWidth;
568
- left = boundingRect.left;
569
- }
570
- else if (step.placement === 'bottom') {
571
- top = boundingRect.bottom + this.opt.arrowWidth;
572
- left = boundingRect.left;
573
- }
574
- else if (step.placement === 'left') {
575
- top = boundingRect.top;
576
- left = boundingRect.left - bubbleBoundingWidth - this.opt.arrowWidth;
577
- }
578
- else if (step.placement === 'right') {
579
- top = boundingRect.top;
580
- left = boundingRect.right + this.opt.arrowWidth;
581
- }
582
- else {
583
- throw 'Bubble placement failed because step.placement is invalid or undefined!';
584
- }
585
-
586
- // UPDATE HIGHLIGHT
587
- self.highlight.setPosition(step, boundingRect);
588
-
589
-
590
- // SET (OR RESET) ARROW OFFSETS
591
- if (step.arrowOffset !== 'center') {
592
- arrowOffset = utils.getPixelValue(step.arrowOffset);
593
- }
594
- else {
595
- arrowOffset = step.arrowOffset;
596
- }
597
- if (!arrowOffset) {
598
- arrowEl.style.top = '';
599
- arrowEl.style.left = '';
600
- }
601
- else if (step.placement === 'top' || step.placement === 'bottom') {
602
- arrowEl.style.top = '';
603
- if (arrowOffset === 'center') {
604
- arrowEl.style.left = Math.floor((bubbleBoundingWidth / 2) - arrowEl.offsetWidth/2) + 'px';
605
- }
606
- else {
607
- // Numeric pixel value
608
- arrowEl.style.left = arrowOffset + 'px';
609
- }
610
- }
611
- else if (step.placement === 'left' || step.placement === 'right') {
612
- arrowEl.style.left = '';
613
- if (arrowOffset === 'center') {
614
- arrowEl.style.top = Math.floor((bubbleBoundingHeight / 2) - arrowEl.offsetHeight/2) + 'px';
615
- }
616
- else {
617
- // Numeric pixel value
618
- arrowEl.style.top = arrowOffset + 'px';
619
- }
620
- }
621
-
622
- // HORIZONTAL OFFSET
623
- if (step.xOffset === 'center') {
624
- left = (boundingRect.left + targetEl.offsetWidth/2) - (bubbleBoundingWidth / 2);
625
- }
626
- else {
627
- left += utils.getPixelValue(step.xOffset);
628
- }
629
- // VERTICAL OFFSET
630
- if (step.yOffset === 'center') {
631
- top = (boundingRect.top + targetEl.offsetHeight/2) - (bubbleBoundingHeight / 2);
632
- }
633
- else {
634
- top += utils.getPixelValue(step.yOffset);
635
- }
636
-
637
- // ADJUST TOP FOR SCROLL POSITION
638
- if (!step.fixedElement) {
639
- top += utils.getScrollTop();
640
- left += utils.getScrollLeft();
641
- }
642
-
643
- // ACCOUNT FOR FIXED POSITION ELEMENTS
644
- el.style.position = (step.fixedElement ? 'fixed' : 'absolute');
645
-
646
- el.style.top = top + 'px';
647
- el.style.left = left + 'px';
648
- },
649
-
650
- /**
651
- * Renders the bubble according to the step JSON.
652
- *
653
- * @param {Object} step Information defining how the bubble should look.
654
- * @param {Number} idx The index of the step in the tour. Not used for callouts.
655
- * @param {Function} callback Function to be invoked after rendering is finished.
656
- */
657
- render: function(step, idx, callback) {
658
- var el = this.element,
659
- tourSpecificRenderer,
660
- customTourData,
661
- unsafe,
662
- currTour,
663
- totalSteps,
664
- nextBtnText,
665
- isLast,
666
- opts;
667
-
668
- // Cache current step information.
669
- if (step) {
670
- this.currStep = step;
671
- }
672
- else if (this.currStep) {
673
- step = this.currStep;
674
- }
675
-
676
- // update highlight with current step information
677
- this.highlight.render(step);
678
-
679
- // Check current tour for total number of steps and custom render data
680
- if(this.opt.isTourBubble){
681
- currTour = winHopscotch.getCurrTour();
682
- if(currTour){
683
- customTourData = currTour.customData;
684
- tourSpecificRenderer = currTour.customRenderer;
685
- unsafe = currTour.unsafe;
686
- if(Array.isArray(currTour.steps)){
687
- totalSteps = currTour.steps.length;
688
- isLast = (idx === totalSteps - 1);
689
- }
690
- }
691
- }else{
692
- customTourData = step.customData;
693
- tourSpecificRenderer = step.customRenderer;
694
- unsafe = step.unsafe;
695
- }
696
-
697
- // Determine label for next button
698
- if(isLast){
699
- nextBtnText = utils.getI18NString('doneBtn');
700
- } else if(step.showSkip) {
701
- nextBtnText = utils.getI18NString('skipBtn');
702
- } else {
703
- nextBtnText = utils.getI18NString('nextBtn');
704
- }
705
-
706
- // Originally called it orientation, but placement is more intuitive.
707
- // Allowing both for now for backwards compatibility.
708
- if (!step.placement && step.orientation) {
709
- step.placement = step.orientation;
710
- }
711
- this.placement = step.placement;
712
-
713
- // Setup the configuration options we want to pass along to the template
714
- opts = {
715
- i18n: {
716
- prevBtn: utils.getI18NString('prevBtn'),
717
- nextBtn: nextBtnText,
718
- closeTooltip: utils.getI18NString('closeTooltip'),
719
- stepNum: this._getStepI18nNum(idx),
720
- },
721
- buttons:{
722
- showPrev: (utils.valOrDefault(step.showPrevButton, this.opt.showPrevButton) && (idx > 0)),
723
- showNext: utils.valOrDefault(step.showNextButton, this.opt.showNextButton),
724
- showCTA: utils.valOrDefault((step.showCTAButton && step.ctaLabel), false),
725
- ctaLabel: step.ctaLabel,
726
- showClose: utils.valOrDefault(this.opt.showCloseButton, true)
727
- },
728
- step:{
729
- num: idx,
730
- isLast: utils.valOrDefault(isLast, false),
731
- title: (step.title || ''),
732
- content: (step.content || ''),
733
- placement: step.placement,
734
- padding: utils.valOrDefault(step.padding, this.opt.bubblePadding),
735
- width: utils.getPixelValue(step.width) || this.opt.bubbleWidth,
736
- customData: (step.customData || {})
737
- },
738
- tour:{
739
- isTour: this.opt.isTourBubble,
740
- numSteps: totalSteps,
741
- unsafe: utils.valOrDefault(unsafe, false),
742
- customData: (customTourData || {})
743
- }
744
- };
745
-
746
- // Render the bubble's content.
747
- // Use tour renderer if available, then the global customRenderer if defined.
748
- if(typeof tourSpecificRenderer === 'function'){
749
- el.innerHTML = tourSpecificRenderer(opts);
750
- }
751
- else if(typeof tourSpecificRenderer === 'string'){
752
- if(!hopscotch.templates || (typeof hopscotch.templates[tourSpecificRenderer] !== 'function')){
753
- throw 'Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.';
754
- }
755
- el.innerHTML = hopscotch.templates[tourSpecificRenderer](opts);
756
- }
757
- else if(customRenderer){
758
- el.innerHTML = customRenderer(opts);
759
- }
760
- else{
761
- if(!hopscotch.templates || (typeof hopscotch.templates[templateToUse] !== 'function')){
762
- throw 'Bubble rendering failed - template "' + templateToUse + '" is not a function.';
763
- }
764
- el.innerHTML = hopscotch.templates[templateToUse](opts);
765
- }
766
-
767
- // Find arrow among new child elements.
768
- children = el.children;
769
- numChildren = children.length;
770
- for (i = 0; i < numChildren; i++){
771
- node = children[i];
772
-
773
- if(utils.hasClass(node, 'hopscotch-arrow')){
774
- this.arrowEl = node;
775
- }
776
- }
777
-
778
- // Set z-index and arrow placement
779
- el.style.zIndex = step.zindex || '';
780
- this._setArrow(step.placement);
781
-
782
- // Set bubble positioning
783
- // Make sure we're using visibility:hidden instead of display:none for height/width calculations.
784
- this.hide(false);
785
- this.setPosition(step);
786
-
787
- // only want to adjust window scroll for non-fixed elements
788
- if (callback) {
789
- callback(!step.fixedElement);
790
- }
791
-
792
- return this;
793
- },
794
-
795
- /**
796
- * Get the I18N step number for the current step.
797
- *
798
- * @private
799
- */
800
- _getStepI18nNum: function(idx) {
801
- var stepNumI18N = utils.getI18NString('stepNums');
802
- if (stepNumI18N && idx < stepNumI18N.length) {
803
- idx = stepNumI18N[idx];
804
- }
805
- else {
806
- idx = idx + 1;
807
- }
808
- return idx;
809
- },
810
-
811
- /**
812
- * Sets which side the arrow is on.
813
- *
814
- * @private
815
- */
816
- _setArrow: function(orientation) {
817
- utils.removeClass(this.arrowEl, 'down up right left');
818
-
819
- // Whatever the orientation is, we want to arrow to appear
820
- // "opposite" of the orientation. E.g., a top orientation
821
- // requires a bottom arrow.
822
- if (orientation === 'top') {
823
- utils.addClass(this.arrowEl, 'down');
824
- }
825
- else if (orientation === 'bottom') {
826
- utils.addClass(this.arrowEl, 'up');
827
- }
828
- else if (orientation === 'left') {
829
- utils.addClass(this.arrowEl, 'right');
830
- }
831
- else if (orientation === 'right') {
832
- utils.addClass(this.arrowEl, 'left');
833
- }
834
- },
835
-
836
- /**
837
- * @private
838
- */
839
- _getArrowDirection: function() {
840
- if (this.placement === 'top') {
841
- return 'down';
842
- }
843
- if (this.placement === 'bottom') {
844
- return 'up';
845
- }
846
- if (this.placement === 'left') {
847
- return 'right';
848
- }
849
- if (this.placement === 'right') {
850
- return 'left';
851
- }
852
- },
853
-
854
- show: function() {
855
- var self = this,
856
- fadeClass = 'fade-in-' + this._getArrowDirection(),
857
- fadeDur = 1000;
858
-
859
- utils.removeClass(this.element, 'hide');
860
- utils.addClass(this.element, fadeClass);
861
- setTimeout(function() {
862
- utils.removeClass(self.element, 'invisible');
863
- }, 50);
864
- setTimeout(function() {
865
- utils.removeClass(self.element, fadeClass);
866
- }, fadeDur);
867
- this.isShowing = true;
868
- this.highlight.show();
869
-
870
- return this;
871
- },
872
-
873
- hide: function(remove) {
874
- var el = this.element;
875
-
876
- remove = utils.valOrDefault(remove, true);
877
- el.style.top = '';
878
- el.style.left = '';
879
-
880
- // display: none
881
- if (remove) {
882
- utils.addClass(el, 'hide');
883
- utils.removeClass(el, 'invisible');
884
- }
885
- // opacity: 0
886
- else {
887
- utils.removeClass(el, 'hide');
888
- utils.addClass(el, 'invisible');
889
- }
890
- utils.removeClass(el, 'animate fade-in-up fade-in-down fade-in-right fade-in-left');
891
- this.isShowing = false;
892
- this.highlight.hide();
893
-
894
- return this;
895
- },
896
-
897
- destroy: function() {
898
- var el = this.element;
899
-
900
- if (el) {
901
- el.parentNode.removeChild(el);
902
- }
903
- utils.removeEvtListener(el, 'click', this.clickCb);
904
- },
905
-
906
- _handleBubbleClick: function(evt){
907
- var action;
908
-
909
- //Recursively look up the parent tree until we find a match
910
- //with one of the classes we're looking for, or the triggering element.
911
- function findMatchRecur(el){
912
- /* We're going to make the assumption that we're not binding
913
- * multiple event classes to the same element.
914
- * (next + previous = wait... err... what?)
915
- *
916
- * In the odd event we end up with an element with multiple
917
- * possible matches, the following priority order is applied:
918
- * hopscotch-cta, hopscotch-next, hopscotch-prev, hopscotch-close
919
- */
920
- if(el === evt.currentTarget){ return null; }
921
- if(utils.hasClass(el, 'hopscotch-cta')){ return 'cta'; }
922
- if(utils.hasClass(el, 'hopscotch-next')){ return 'next'; }
923
- if(utils.hasClass(el, 'hopscotch-prev')){ return 'prev'; }
924
- if(utils.hasClass(el, 'hopscotch-close')){ return 'close'; }
925
- /*else*/ return findMatchRecur(el.parentElement);
926
- }
927
-
928
- action = findMatchRecur(evt.target);
929
-
930
- //Now that we know what action we should take, let's take it.
931
- if (action === 'cta'){
932
- if (!this.opt.isTourBubble) {
933
- // This is a callout. Close the callout when CTA is clicked.
934
- winHopscotch.getCalloutManager().removeCallout(this.currStep.id);
935
- }
936
- // Call onCTA callback if one is provided
937
- if (this.currStep.onCTA) {
938
- utils.invokeCallback(this.currStep.onCTA);
939
- }
940
- }
941
- else if (action === 'next'){
942
- winHopscotch.nextStep(true);
943
- }
944
- else if (action === 'prev'){
945
- winHopscotch.prevStep(true);
946
- }
947
- else if (action === 'close'){
948
- if (this.opt.isTourBubble){
949
- var currStepNum = winHopscotch.getCurrStepNum(),
950
- currTour = winHopscotch.getCurrTour(),
951
- doEndCallback = (currStepNum === currTour.steps.length-1);
952
-
953
- utils.invokeEventCallbacks('close');
954
-
955
- winHopscotch.endTour(true, doEndCallback);
956
- } else {
957
- if (this.opt.onClose) {
958
- utils.invokeCallback(this.opt.onClose);
959
- }
960
- if (this.opt.id && !this.opt.isTourBubble) {
961
- // Remove via the HopscotchCalloutManager.
962
- // removeCallout() calls HopscotchBubble.destroy internally.
963
- winHopscotch.getCalloutManager().removeCallout(this.opt.id);
964
- }
965
- else {
966
- this.destroy();
967
- }
968
- }
969
-
970
- utils.evtPreventDefault(evt);
971
- }
972
- //Otherwise, do nothing. We didn't click on anything relevant.
973
- },
974
-
975
- init: function(initOpt) {
976
- var el = document.createElement('div'),
977
- self = this,
978
- resizeCooldown = false, // for updating after window resize
979
- onWinResize,
980
- appendToBody,
981
- children,
982
- numChildren,
983
- node,
984
- i,
985
- opt;
986
-
987
- //Register DOM element for this bubble.
988
- this.element = el;
989
-
990
- //Merge bubble options with defaults.
991
- opt = {
992
- showPrevButton: defaultOpts.showPrevButton,
993
- showNextButton: defaultOpts.showNextButton,
994
- bubbleWidth: defaultOpts.bubbleWidth,
995
- bubblePadding: defaultOpts.bubblePadding,
996
- arrowWidth: defaultOpts.arrowWidth,
997
- showNumber: true,
998
- isTourBubble: true
999
- };
1000
- initOpt = (typeof initOpt === undefinedStr ? {} : initOpt);
1001
- utils.extend(opt, initOpt);
1002
- this.opt = opt;
1003
-
1004
- //Apply classes to bubble. Add "animated" for fade css animation
1005
- el.className = 'hopscotch-bubble animated';
1006
- if (!opt.isTourBubble) {
1007
- utils.addClass(el, 'hopscotch-callout no-number');
1008
- }
1009
-
1010
- self.highlight = new HopscotchHighlight(initOpt);
1011
-
1012
- /**
1013
- * Not pretty, but IE8 doesn't support Function.bind(), so I'm
1014
- * relying on closures to keep a handle of "this".
1015
- * Reset position of bubble when window is resized
1016
- *
1017
- * @private
1018
- */
1019
- onWinResize = function() {
1020
- if (resizeCooldown || !self.isShowing) {
1021
- return;
1022
- }
1023
-
1024
- resizeCooldown = true;
1025
- setTimeout(function() {
1026
- self.setPosition(self.currStep);
1027
- resizeCooldown = false;
1028
- }, 100);
1029
- };
1030
-
1031
- //Add listener to reset bubble position on window resize
1032
- utils.addEvtListener(window, 'resize', onWinResize);
1033
-
1034
- //Create our click callback handler and keep a
1035
- //reference to it for later.
1036
- this.clickCb = function(evt){
1037
- self._handleBubbleClick(evt);
1038
- };
1039
- utils.addEvtListener(el, 'click', this.clickCb);
1040
-
1041
- //Hide the bubble by default
1042
- this.hide();
1043
-
1044
- //Finally, append our new bubble to body once the DOM is ready.
1045
- if (utils.documentIsReady()) {
1046
- document.body.appendChild(el);
1047
- self.highlight.addToDom();
1048
- }
1049
- else {
1050
- // Moz, webkit, Opera
1051
- if (document.addEventListener) {
1052
- appendToBody = function() {
1053
- document.removeEventListener('DOMContentLoaded', appendToBody);
1054
- window.removeEventListener('load', appendToBody);
1055
-
1056
- document.body.appendChild(el);
1057
- self.highlight.addToDom();
1058
- };
1059
-
1060
- document.addEventListener('DOMContentLoaded', appendToBody, false);
1061
- }
1062
- // IE
1063
- else {
1064
- appendToBody = function() {
1065
- if (document.readyState === 'complete') {
1066
- document.detachEvent('onreadystatechange', appendToBody);
1067
- window.detachEvent('onload', appendToBody);
1068
- document.body.appendChild(el);
1069
- self.highlight.addToDom();
1070
- }
1071
- };
1072
-
1073
- document.attachEvent('onreadystatechange', appendToBody);
1074
- }
1075
- utils.addEvtListener(window, 'load', appendToBody);
1076
- }
1077
- }
1078
- };
1079
-
1080
- /**
1081
- * HopscotchCalloutManager
1082
- *
1083
- * @class Manages the creation and destruction of single callouts.
1084
- * @constructor
1085
- */
1086
- HopscotchCalloutManager = function() {
1087
- var callouts = {};
1088
-
1089
- /**
1090
- * createCallout
1091
- *
1092
- * Creates a standalone callout. This callout has the same API
1093
- * as a Hopscotch tour bubble.
1094
- *
1095
- * @param {Object} opt The options for the callout. For the most
1096
- * part, these are the same options as you would find in a tour
1097
- * step.
1098
- */
1099
- this.createCallout = function(opt) {
1100
- var callout;
1101
-
1102
- if (opt.id) {
1103
- if (callouts[opt.id]) {
1104
- throw 'Callout by that id already exists. Please choose a unique id.';
1105
- }
1106
- opt.showNextButton = opt.showPrevButton = false;
1107
- opt.isTourBubble = false;
1108
- callout = new HopscotchBubble(opt);
1109
- callouts[opt.id] = callout;
1110
- if (opt.target) {
1111
- callout.render(opt, null, function() {
1112
- callout.show();
1113
- });
1114
- }
1115
- }
1116
- else {
1117
- throw 'Must specify a callout id.';
1118
- }
1119
- return callout;
1120
- };
1121
-
1122
- /**
1123
- * getCallout
1124
- *
1125
- * Returns a callout by its id.
1126
- *
1127
- * @param {String} id The id of the callout to fetch.
1128
- * @returns {Object} HopscotchBubble
1129
- */
1130
- this.getCallout = function(id) {
1131
- return callouts[id];
1132
- };
1133
-
1134
- /**
1135
- * removeAllCallouts
1136
- *
1137
- * Removes all existing callouts.
1138
- */
1139
- this.removeAllCallouts = function() {
1140
- var calloutId,
1141
- callout;
1142
-
1143
- for (calloutId in callouts) {
1144
- if (callouts.hasOwnProperty(calloutId)) {
1145
- this.removeCallout(calloutId);
1146
- }
1147
- }
1148
- };
1149
-
1150
- /**
1151
- * removeAllCallout
1152
- *
1153
- * Removes an existing callout by id.
1154
- *
1155
- * @param {String} id The id of the callout to remove.
1156
- */
1157
- this.removeCallout = function(id) {
1158
- var callout = callouts[id];
1159
-
1160
- callouts[id] = null;
1161
- if (!callout) { return; }
1162
-
1163
- callout.destroy();
1164
- };
1165
- };
1166
-
1167
- /**
1168
- * Hopscotch
1169
- *
1170
- * @class Creates the Hopscotch object. Used to manage tour progress and configurations.
1171
- * @constructor
1172
- * @param {Object} initOptions Options to be passed to `configure()`.
1173
- */
1174
- Hopscotch = function(initOptions) {
1175
- var self = this, // for targetClickNextFn
1176
- bubble,
1177
- calloutMgr,
1178
- opt,
1179
- currTour,
1180
- currStepNum,
1181
- cookieTourId,
1182
- cookieTourStep,
1183
- _configure,
1184
-
1185
- /**
1186
- * getBubble
1187
- *
1188
- * Singleton accessor function for retrieving or creating bubble object.
1189
- *
1190
- * @private
1191
- * @param setOptions {Boolean} when true, transfers configuration options to the bubble
1192
- * @returns {Object} HopscotchBubble
1193
- */
1194
- getBubble = function(setOptions) {
1195
- if (!bubble) {
1196
- bubble = new HopscotchBubble(opt);
1197
- }
1198
- if (setOptions) {
1199
- utils.extend(bubble.opt, {
1200
- bubblePadding: getOption('bubblePadding'),
1201
- bubbleWidth: getOption('bubbleWidth'),
1202
- showNextButton: getOption('showNextButton'),
1203
- showPrevButton: getOption('showPrevButton'),
1204
- showCloseButton: getOption('showCloseButton'),
1205
- arrowWidth: getOption('arrowWidth')
1206
- });
1207
- }
1208
- return bubble;
1209
- },
1210
-
1211
- /**
1212
- * Convenience method for getting an option. Returns custom config option
1213
- * or the default config option if no custom value exists.
1214
- *
1215
- * @private
1216
- * @param name {String} config option name
1217
- * @returns {Object} config option value
1218
- */
1219
- getOption = function(name) {
1220
- if (typeof opt === 'undefined') {
1221
- return defaultOpts[name];
1222
- }
1223
- return utils.valOrDefault(opt[name], defaultOpts[name]);
1224
- },
1225
-
1226
- /**
1227
- * getCurrStep
1228
- *
1229
- * @private
1230
- * @returns {Object} the step object corresponding to the current value of currStepNum
1231
- */
1232
- getCurrStep = function() {
1233
- var step;
1234
-
1235
- if (currStepNum < 0 || currStepNum >= currTour.steps.length) {
1236
- step = null;
1237
- }
1238
- else {
1239
- step = currTour.steps[currStepNum];
1240
- }
1241
-
1242
- return step;
1243
- },
1244
-
1245
- /**
1246
- * Used for nextOnTargetClick
1247
- *
1248
- * @private
1249
- */
1250
- targetClickNextFn = function() {
1251
- self.nextStep();
1252
- },
1253
-
1254
- /**
1255
- * adjustWindowScroll
1256
- *
1257
- * Checks if the bubble or target element is partially or completely
1258
- * outside of the viewport. If it is, adjust the window scroll position
1259
- * to bring it back into the viewport.
1260
- *
1261
- * @private
1262
- * @param {Function} cb Callback to invoke after done scrolling.
1263
- */
1264
- adjustWindowScroll = function(cb) {
1265
- var bubble = getBubble(),
1266
-
1267
- // Calculate the bubble element top and bottom position
1268
- bubbleEl = bubble.element,
1269
- bubbleTop = utils.getPixelValue(bubbleEl.style.top),
1270
- bubbleBottom = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight),
1271
-
1272
- // Calculate the target element top and bottom position
1273
- targetEl = utils.getStepTarget(getCurrStep()),
1274
- targetBounds = targetEl.getBoundingClientRect(),
1275
- targetElTop = targetBounds.top + utils.getScrollTop(),
1276
- targetElBottom = targetBounds.bottom + utils.getScrollTop(),
1277
-
1278
- // The higher of the two: bubble or target
1279
- targetTop = (bubbleTop < targetElTop) ? bubbleTop : targetElTop,
1280
- // The lower of the two: bubble or target
1281
- targetBottom = (bubbleBottom > targetElBottom) ? bubbleBottom : targetElBottom,
1282
-
1283
- // Calculate the current viewport top and bottom
1284
- windowTop = utils.getScrollTop(),
1285
- windowBottom = windowTop + utils.getWindowHeight(),
1286
-
1287
- // This is our final target scroll value.
1288
- scrollToVal = targetTop - getOption('scrollTopMargin'),
1289
-
1290
- scrollEl,
1291
- yuiAnim,
1292
- yuiEase,
1293
- direction,
1294
- scrollIncr,
1295
- scrollTimeout,
1296
- scrollTimeoutFn;
1297
-
1298
- // Target and bubble are both visible in viewport
1299
- if (targetTop >= windowTop && (targetTop <= windowTop + getOption('scrollTopMargin') || targetBottom <= windowBottom)) {
1300
- if (cb) { cb(); } // HopscotchBubble.show
1301
- }
1302
-
1303
- // Abrupt scroll to scroll target
1304
- else if (!getOption('smoothScroll')) {
1305
- window.scrollTo(0, scrollToVal);
1306
-
1307
- if (cb) { cb(); } // HopscotchBubble.show
1308
- }
1309
-
1310
- // Smooth scroll to scroll target
1311
- else {
1312
- // Use YUI if it exists
1313
- if (typeof YAHOO !== undefinedStr &&
1314
- typeof YAHOO.env !== undefinedStr &&
1315
- typeof YAHOO.env.ua !== undefinedStr &&
1316
- typeof YAHOO.util !== undefinedStr &&
1317
- typeof YAHOO.util.Scroll !== undefinedStr) {
1318
- scrollEl = YAHOO.env.ua.webkit ? document.body : document.documentElement;
1319
- yuiEase = YAHOO.util.Easing ? YAHOO.util.Easing.easeOut : undefined;
1320
- yuiAnim = new YAHOO.util.Scroll(scrollEl, {
1321
- scroll: { to: [0, scrollToVal] }
1322
- }, getOption('scrollDuration')/1000, yuiEase);
1323
- yuiAnim.onComplete.subscribe(cb);
1324
- yuiAnim.animate();
1325
- }
1326
-
1327
- // Use jQuery if it exists
1328
- else if (hasJquery) {
1329
- jQuery('body, html').animate({ scrollTop: scrollToVal }, getOption('scrollDuration'), cb);
1330
- }
1331
-
1332
- // Use my crummy setInterval scroll solution if we're using plain, vanilla Javascript.
1333
- else {
1334
- if (scrollToVal < 0) {
1335
- scrollToVal = 0;
1336
- }
1337
-
1338
- // 48 * 10 == 480ms scroll duration
1339
- // make it slightly less than CSS transition duration because of
1340
- // setInterval overhead.
1341
- // To increase or decrease duration, change the divisor of scrollIncr.
1342
- direction = (windowTop > targetTop) ? -1 : 1; // -1 means scrolling up, 1 means down
1343
- scrollIncr = Math.abs(windowTop - scrollToVal) / (getOption('scrollDuration')/10);
1344
- scrollTimeoutFn = function() {
1345
- var scrollTop = utils.getScrollTop(),
1346
- scrollTarget = scrollTop + (direction * scrollIncr);
1347
-
1348
- if ((direction > 0 && scrollTarget >= scrollToVal) ||
1349
- (direction < 0 && scrollTarget <= scrollToVal)) {
1350
- // Overshot our target. Just manually set to equal the target
1351
- // and clear the interval
1352
- scrollTarget = scrollToVal;
1353
- if (cb) { cb(); } // HopscotchBubble.show
1354
- window.scrollTo(0, scrollTarget);
1355
- return;
1356
- }
1357
-
1358
- window.scrollTo(0, scrollTarget);
1359
-
1360
- if (utils.getScrollTop() === scrollTop) {
1361
- // Couldn't scroll any further.
1362
- if (cb) { cb(); } // HopscotchBubble.show
1363
- return;
1364
- }
1365
-
1366
- // If we reached this point, that means there's still more to scroll.
1367
- setTimeout(scrollTimeoutFn, 10);
1368
- };
1369
-
1370
- scrollTimeoutFn();
1371
- }
1372
- }
1373
- },
1374
-
1375
- /**
1376
- * goToStepWithTarget
1377
- *
1378
- * Helper function to increment the step number until a step is found where
1379
- * the step target exists or until we reach the end/beginning of the tour.
1380
- *
1381
- * @private
1382
- * @param {Number} direction Either 1 for incrementing or -1 for decrementing
1383
- * @param {Function} cb The callback function to be invoked when the step has been found
1384
- */
1385
- goToStepWithTarget = function(direction, cb) {
1386
- var target,
1387
- step,
1388
- goToStepFn;
1389
-
1390
- if (currStepNum + direction >= 0 &&
1391
- currStepNum + direction < currTour.steps.length) {
1392
-
1393
- currStepNum += direction;
1394
- step = getCurrStep();
1395
-
1396
- goToStepFn = function() {
1397
- target = utils.getStepTarget(step);
1398
-
1399
- if (target) {
1400
- // We're done! Return the step number via the callback.
1401
- cb(currStepNum);
1402
- }
1403
- else {
1404
- // Haven't found a valid target yet. Recursively call
1405
- // goToStepWithTarget.
1406
- utils.invokeEventCallbacks('error');
1407
- goToStepWithTarget(direction, cb);
1408
- }
1409
- };
1410
-
1411
- if (step.delay) {
1412
- setTimeout(goToStepFn, step.delay);
1413
- }
1414
- else {
1415
- goToStepFn();
1416
- }
1417
- }
1418
- else {
1419
- cb(-1); // signal that we didn't find any step with a valid target
1420
- }
1421
- },
1422
-
1423
- /**
1424
- * changeStep
1425
- *
1426
- * Helper function to change step by going forwards or backwards 1.
1427
- * nextStep and prevStep are publicly accessible wrappers for this function.
1428
- *
1429
- * @private
1430
- * @param {Boolean} doCallbacks Flag for invoking onNext or onPrev callbacks
1431
- * @param {Number} direction Either 1 for "next" or -1 for "prev"
1432
- */
1433
- changeStep = function(doCallbacks, direction) {
1434
- var bubble = getBubble(),
1435
- self = this,
1436
- step,
1437
- origStep,
1438
- wasMultiPage,
1439
- changeStepCb;
1440
-
1441
- bubble.hide();
1442
-
1443
- doCallbacks = utils.valOrDefault(doCallbacks, true);
1444
- step = getCurrStep();
1445
- origStep = step;
1446
- if (direction > 0) {
1447
- wasMultiPage = origStep.multipage;
1448
- }
1449
- else {
1450
- wasMultiPage = (currStepNum > 0 && currTour.steps[currStepNum-1].multipage);
1451
- }
1452
-
1453
- /**
1454
- * Callback for goToStepWithTarget
1455
- *
1456
- * @private
1457
- */
1458
- changeStepCb = function(stepNum) {
1459
- var doShowFollowingStep;
1460
-
1461
- if (stepNum === -1) {
1462
- // Wasn't able to find a step with an existing element. End tour.
1463
- return this.endTour(true);
1464
- }
1465
-
1466
- if (doCallbacks) {
1467
- if (direction > 0) {
1468
- doShowFollowingStep = utils.invokeEventCallbacks('next', origStep.onNext);
1469
- }
1470
- else {
1471
- doShowFollowingStep = utils.invokeEventCallbacks('prev', origStep.onPrev);
1472
- }
1473
- }
1474
-
1475
- // If the state of the tour is updated in a callback, assume the client
1476
- // doesn't want to go to next step since they specifically updated.
1477
- if (stepNum !== currStepNum) {
1478
- return;
1479
- }
1480
-
1481
- if (wasMultiPage) {
1482
- // Update state for the next page
1483
- utils.setState(getOption('cookieName'), currTour.id + ':' + currStepNum, 1);
1484
-
1485
- // Next step is on a different page, so no need to attempt to render it.
1486
- return;
1487
- }
1488
-
1489
- doShowFollowingStep = utils.valOrDefault(doShowFollowingStep, true);
1490
-
1491
- // If the onNext/onPrev callback returned false, halt the tour and
1492
- // don't show the next step.
1493
- if (doShowFollowingStep) {
1494
- this.showStep(stepNum);
1495
- }
1496
- else {
1497
- // Halt tour (but don't clear state)
1498
- this.endTour(false);
1499
- }
1500
- };
1501
-
1502
- if (!wasMultiPage && getOption('skipIfNoElement')) {
1503
- goToStepWithTarget(direction, function(stepNum) {
1504
- changeStepCb.call(self, stepNum);
1505
- });
1506
- }
1507
- else if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) {
1508
- // only try incrementing once, and invoke error callback if no target is found
1509
- currStepNum += direction;
1510
- step = getCurrStep();
1511
- if (!utils.getStepTarget(step) && !wasMultiPage) {
1512
- utils.invokeEventCallbacks('error');
1513
- return this.endTour(true, false);
1514
- }
1515
- changeStepCb.call(this, currStepNum);
1516
- }
1517
-
1518
- return this;
1519
- },
1520
-
1521
- /**
1522
- * loadTour
1523
- *
1524
- * Loads, but does not display, tour.
1525
- *
1526
- * @private
1527
- * @param tour The tour JSON object
1528
- */
1529
- loadTour = function(tour) {
1530
- var tmpOpt = {},
1531
- prop,
1532
- tourState,
1533
- tourPair;
1534
-
1535
- // Set tour-specific configurations
1536
- for (prop in tour) {
1537
- if (tour.hasOwnProperty(prop) &&
1538
- prop !== 'id' &&
1539
- prop !== 'steps') {
1540
- tmpOpt[prop] = tour[prop];
1541
- }
1542
- }
1543
-
1544
- //this.resetDefaultOptions(); // reset all options so there are no surprises
1545
- // TODO check number of config properties of tour
1546
- _configure.call(this, tmpOpt, true);
1547
-
1548
- // Get existing tour state, if it exists.
1549
- tourState = utils.getState(getOption('cookieName'));
1550
- if (tourState) {
1551
- tourPair = tourState.split(':');
1552
- cookieTourId = tourPair[0]; // selecting tour is not supported by this framework.
1553
- cookieTourStep = tourPair[1];
1554
-
1555
- cookieTourStep = parseInt(cookieTourStep, 10);
1556
- }
1557
-
1558
- return this;
1559
- },
1560
-
1561
- /**
1562
- * Find the first step to show for a tour. (What is the first step with a
1563
- * target on the page?)
1564
- */
1565
- findStartingStep = function(startStepNum, cb) {
1566
- var step,
1567
- target,
1568
- stepNum;
1569
-
1570
- currStepNum = startStepNum || 0;
1571
- step = getCurrStep();
1572
- target = utils.getStepTarget(step);
1573
-
1574
- if (target) {
1575
- // First step had an existing target.
1576
- cb(currStepNum);
1577
- return;
1578
- }
1579
-
1580
- if (!target) {
1581
- // Previous target doesn't exist either. The user may have just
1582
- // clicked on a link that wasn't part of the tour. Another possibility is that
1583
- // the user clicked on the correct link, but the target is just missing for
1584
- // whatever reason. In either case, we should just advance until we find a step
1585
- // that has a target on the page or end the tour if we can't find such a step.
1586
- utils.invokeEventCallbacks('error');
1587
-
1588
- if (getOption('skipIfNoElement')) {
1589
- goToStepWithTarget(1, cb);
1590
- return;
1591
- }
1592
- else {
1593
- currStepNum = -1;
1594
- cb(currStepNum);
1595
- }
1596
- }
1597
- },
1598
-
1599
- showStepHelper = function(stepNum) {
1600
- var step = currTour.steps[stepNum],
1601
- tourSteps = currTour.steps,
1602
- numTourSteps = tourSteps.length,
1603
- cookieVal = currTour.id + ':' + stepNum,
1604
- bubble = getBubble(),
1605
- targetEl = utils.getStepTarget(step),
1606
- isLast,
1607
- showBubble;
1608
-
1609
- showBubble = function() {
1610
- bubble.show();
1611
- utils.invokeEventCallbacks('show', step.onShow);
1612
- };
1613
-
1614
- // Update bubble for current step
1615
- currStepNum = stepNum;
1616
-
1617
- bubble.hide(false);
1618
-
1619
- isLast = (stepNum === numTourSteps - 1);
1620
- bubble.render(step, stepNum, function(adjustScroll) {
1621
- // when done adjusting window scroll, call showBubble helper fn
1622
- if (adjustScroll) {
1623
- adjustWindowScroll(showBubble);
1624
- }
1625
- else {
1626
- showBubble();
1627
- }
1628
-
1629
- // If we want to advance to next step when user clicks on target.
1630
- if (step.nextOnTargetClick) {
1631
- utils.addEvtListener(targetEl, 'click', targetClickNextFn);
1632
- }
1633
- });
1634
-
1635
- utils.setState(getOption('cookieName'), cookieVal, 1);
1636
- },
1637
-
1638
- /**
1639
- * init
1640
- *
1641
- * Initializes the Hopscotch object.
1642
- *
1643
- * @private
1644
- */
1645
- init = function(initOptions) {
1646
- if (initOptions) {
1647
- //initOptions.cookieName = initOptions.cookieName || 'hopscotch.tour.state';
1648
- this.configure(initOptions);
1649
- }
1650
- };
1651
-
1652
- /**
1653
- * getCalloutManager
1654
- *
1655
- * Gets the callout manager.
1656
- *
1657
- * @returns {Object} HopscotchCalloutManager
1658
- *
1659
- */
1660
- this.getCalloutManager = function() {
1661
- if (typeof calloutMgr === undefinedStr) {
1662
- calloutMgr = new HopscotchCalloutManager();
1663
- }
1664
-
1665
- return calloutMgr;
1666
- };
1667
-
1668
- /**
1669
- * startTour
1670
- *
1671
- * Begins the tour.
1672
- *
1673
- * @param {Object} tour The tour JSON object
1674
- * @stepNum {Number} stepNum __Optional__ The step number to start from
1675
- * @returns {Object} Hopscotch
1676
- *
1677
- */
1678
- this.startTour = function(tour, stepNum) {
1679
- var bubble,
1680
- currStepNum,
1681
- self = this;
1682
-
1683
- // loadTour if we are calling startTour directly. (When we call startTour
1684
- // from window onLoad handler, we'll use currTour)
1685
- if (!currTour) {
1686
- currTour = tour;
1687
- loadTour.call(this, tour);
1688
- }
1689
-
1690
- if (typeof stepNum !== undefinedStr) {
1691
- if (stepNum >= currTour.steps.length) {
1692
- throw 'Specified step number out of bounds.';
1693
- }
1694
- currStepNum = stepNum;
1695
- }
1696
-
1697
- // If document isn't ready, wait for it to finish loading.
1698
- // (so that we can calculate positioning accurately)
1699
- if (!utils.documentIsReady()) {
1700
- waitingToStart = true;
1701
- return this;
1702
- }
1703
-
1704
- if (typeof currStepNum === "undefined" && currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) {
1705
- currStepNum = cookieTourStep;
1706
- }
1707
- else if (!currStepNum) {
1708
- currStepNum = 0;
1709
- }
1710
-
1711
- // Find the current step we should begin the tour on, and then actually start the tour.
1712
- findStartingStep(currStepNum, function(stepNum) {
1713
- var target = (stepNum !== -1) && utils.getStepTarget(currTour.steps[stepNum]);
1714
-
1715
- if (!target) {
1716
- // Should we trigger onEnd callback? Let's err on the side of caution
1717
- // and not trigger it. Don't want weird stuff happening on a page that
1718
- // wasn't meant for the tour. Up to the developer to fix their tour.
1719
- self.endTour(false, false);
1720
- return;
1721
- }
1722
-
1723
- utils.invokeEventCallbacks('start');
1724
-
1725
- bubble = getBubble();
1726
- // TODO: do we still need this call to .hide()? No longer using opt.animate...
1727
- // Leaving it in for now to play it safe
1728
- bubble.hide(false); // make invisible for boundingRect calculations when opt.animate == true
1729
-
1730
- self.isActive = true;
1731
-
1732
- if (!utils.getStepTarget(getCurrStep())) {
1733
- // First step element doesn't exist
1734
- utils.invokeEventCallbacks('error');
1735
- if (getOption('skipIfNoElement')) {
1736
- self.nextStep(false);
1737
- }
1738
- }
1739
- else {
1740
- self.showStep(stepNum);
1741
- }
1742
- });
1743
-
1744
- return this;
1745
- };
1746
-
1747
- /**
1748
- * showStep
1749
- *
1750
- * Skips to a specific step and renders the corresponding bubble.
1751
- *
1752
- * @stepNum {Number} stepNum The step number to show
1753
- * @returns {Object} Hopscotch
1754
- */
1755
- this.showStep = function(stepNum) {
1756
- var step = currTour.steps[stepNum];
1757
- if (step.delay) {
1758
- setTimeout(function() {
1759
- showStepHelper(stepNum);
1760
- }, step.delay);
1761
- }
1762
- else {
1763
- showStepHelper(stepNum);
1764
- }
1765
- return this;
1766
- };
1767
-
1768
- /**
1769
- * prevStep
1770
- *
1771
- * Jump to the previous step.
1772
- *
1773
- * @param {Boolean} doCallbacks Flag for invoking onPrev callback. Defaults to true.
1774
- * @returns {Object} Hopscotch
1775
- */
1776
- this.prevStep = function(doCallbacks) {
1777
- changeStep.call(this, doCallbacks, -1);
1778
- return this;
1779
- };
1780
-
1781
- /**
1782
- * nextStep
1783
- *
1784
- * Jump to the next step.
1785
- *
1786
- * @param {Boolean} doCallbacks Flag for invoking onNext callback. Defaults to true.
1787
- * @returns {Object} Hopscotch
1788
- */
1789
- this.nextStep = function(doCallbacks) {
1790
- var step = getCurrStep(),
1791
- targetEl = utils.getStepTarget(step);
1792
-
1793
- if (step.nextOnTargetClick) {
1794
- // Detach the listener after we've clicked on the target OR the next button.
1795
- utils.removeEvtListener(targetEl, 'click', targetClickNextFn);
1796
- }
1797
- changeStep.call(this, doCallbacks, 1);
1798
- return this;
1799
- };
1800
-
1801
- /**
1802
- * endTour
1803
- *
1804
- * Cancels out of an active tour.
1805
- *
1806
- * @param {Boolean} clearState Flag for clearing state. Defaults to true.
1807
- * @param {Boolean} doCallbacks Flag for invoking 'onEnd' callbacks. Defaults to true.
1808
- * @returns {Object} Hopscotch
1809
- */
1810
- this.endTour = function(clearState, doCallbacks) {
1811
- var bubble = getBubble();
1812
- clearState = utils.valOrDefault(clearState, true);
1813
- doCallbacks = utils.valOrDefault(doCallbacks, true);
1814
- currStepNum = 0;
1815
- cookieTourStep = undefined;
1816
-
1817
- bubble.hide();
1818
- if (clearState) {
1819
- utils.clearState(getOption('cookieName'));
1820
- }
1821
- if (this.isActive) {
1822
- this.isActive = false;
1823
-
1824
- if (currTour && doCallbacks) {
1825
- utils.invokeEventCallbacks('end');
1826
- }
1827
- }
1828
-
1829
- this.removeCallbacks(null, true);
1830
- this.resetDefaultOptions();
1831
-
1832
- currTour = null;
1833
-
1834
- return this;
1835
- };
1836
-
1837
- /**
1838
- * getCurrTour
1839
- *
1840
- * @return {Object} The currently loaded tour.
1841
- */
1842
- this.getCurrTour = function() {
1843
- return currTour;
1844
- };
1845
-
1846
- /**
1847
- * getCurrTarget
1848
- *
1849
- * @return {Object} The currently visible target.
1850
- */
1851
- this.getCurrTarget = function() {
1852
- return utils.getStepTarget(getCurrStep());
1853
- };
1854
-
1855
- /**
1856
- * getCurrStepNum
1857
- *
1858
- * @return {number} The current zero-based step number.
1859
- */
1860
- this.getCurrStepNum = function() {
1861
- return currStepNum;
1862
- };
1863
-
1864
- /**
1865
- * refreshBubblePosition
1866
- *
1867
- * Tell hopscotch that the position of the current tour element changed
1868
- * and the bubble therefore needs to be redrawn
1869
- *
1870
- * @returns {Object} Hopscotch
1871
- */
1872
- this.refreshBubblePosition = function() {
1873
- bubble.setPosition(getCurrStep());
1874
- return this;
1875
- };
1876
-
1877
- /**
1878
- * listen
1879
- *
1880
- * Adds a callback for one of the event types. Valid event types are:
1881
- *
1882
- * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
1883
- * @param {Function} cb The callback to add.
1884
- * @param {Boolean} isTourCb Flag indicating callback is from a tour definition.
1885
- * For internal use only!
1886
- * @returns {Object} Hopscotch
1887
- */
1888
- this.listen = function(evtType, cb, isTourCb) {
1889
- if (evtType) {
1890
- callbacks[evtType].push({ cb: cb, fromTour: isTourCb });
1891
- }
1892
- return this;
1893
- };
1894
-
1895
- /**
1896
- * unlisten
1897
- *
1898
- * Removes a callback for one of the event types, e.g. 'start', 'next', etc.
1899
- *
1900
- * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
1901
- * @param {Function} cb The callback to remove.
1902
- * @returns {Object} Hopscotch
1903
- */
1904
- this.unlisten = function(evtType, cb) {
1905
- var evtCallbacks = callbacks[evtType],
1906
- i,
1907
- len;
1908
-
1909
- for (i = 0, len = evtCallbacks.length; i < len; ++i) {
1910
- if (evtCallbacks[i] === cb) {
1911
- evtCallbacks.splice(i, 1);
1912
- }
1913
- }
1914
- return this;
1915
- };
1916
-
1917
- /**
1918
- * removeCallbacks
1919
- *
1920
- * Remove callbacks for hopscotch events. If tourOnly is set to true, only
1921
- * removes callbacks specified by a tour (callbacks set by external calls
1922
- * to hopscotch.configure or hopscotch.listen will not be removed). If
1923
- * evtName is null or undefined, callbacks for all events will be removed.
1924
- *
1925
- * @param {string} evtName Optional Event name for which we should remove callbacks
1926
- * @param {boolean} tourOnly Optional flag to indicate we should only remove callbacks added
1927
- * by a tour. Defaults to false.
1928
- * @returns {Object} Hopscotch
1929
- */
1930
- this.removeCallbacks = function(evtName, tourOnly) {
1931
- var cbArr,
1932
- i,
1933
- len,
1934
- evt;
1935
-
1936
- // If evtName is null or undefined, remove callbacks for all events.
1937
- for (evt in callbacks) {
1938
- if (!evtName || evtName === evt) {
1939
- if (tourOnly) {
1940
- cbArr = callbacks[evt];
1941
- for (i=0, len=cbArr.length; i < len; ++i) {
1942
- if (cbArr[i].fromTour) {
1943
- cbArr.splice(i--, 1);
1944
- --len;
1945
- }
1946
- }
1947
- }
1948
- else {
1949
- callbacks[evt] = [];
1950
- }
1951
- }
1952
- }
1953
- return this;
1954
- };
1955
-
1956
- /**
1957
- * registerHelper
1958
- * ==============
1959
- * Registers a helper function to be used as a callback function.
1960
- *
1961
- * @param {String} id The id of the function.
1962
- * @param {Function} id The callback function.
1963
- */
1964
- this.registerHelper = function(id, fn) {
1965
- if (typeof id === 'string' && typeof fn === 'function') {
1966
- helpers[id] = fn;
1967
- }
1968
- };
1969
-
1970
- this.unregisterHelper = function(id) {
1971
- helpers[id] = null;
1972
- };
1973
-
1974
- this.invokeHelper = function(id) {
1975
- var args = [],
1976
- i,
1977
- len;
1978
-
1979
- for (i = 1, len = arguments.length; i < len; ++i) {
1980
- args.push(arguments[i]);
1981
- }
1982
- if (helpers[id]) {
1983
- helpers[id].call(null, args);
1984
- }
1985
- };
1986
-
1987
- /**
1988
- * setCookieName
1989
- *
1990
- * Sets the cookie name (or sessionStorage name, if supported) used for multi-page
1991
- * tour persistence.
1992
- *
1993
- * @param {String} name The cookie name
1994
- * @returns {Object} Hopscotch
1995
- */
1996
- this.setCookieName = function(name) {
1997
- opt.cookieName = name;
1998
- return this;
1999
- };
2000
-
2001
- /**
2002
- * resetDefaultOptions
2003
- *
2004
- * Resets all configuration options to default.
2005
- *
2006
- * @returns {Object} Hopscotch
2007
- */
2008
- this.resetDefaultOptions = function() {
2009
- opt = {};
2010
- return this;
2011
- };
2012
-
2013
- /**
2014
- * resetDefaultI18N
2015
- *
2016
- * Resets all i18n.
2017
- *
2018
- * @returns {Object} Hopscotch
2019
- */
2020
- this.resetDefaultI18N = function() {
2021
- customI18N = {};
2022
- return this;
2023
- };
2024
-
2025
- /**
2026
- * hasState
2027
- *
2028
- * Returns state from a previous tour run, if it exists.
2029
- *
2030
- * @returns {String} State of previous tour run, or empty string if none exists.
2031
- */
2032
- this.getState = function() {
2033
- return utils.getState(getOption('cookieName'));
2034
- };
2035
-
2036
- /**
2037
- * _configure
2038
- *
2039
- * @see this.configure
2040
- * @private
2041
- * @param options
2042
- * @param {Boolean} isTourOptions Should be set to true when setting options from a tour definition.
2043
- */
2044
- _configure = function(options, isTourOptions) {
2045
- var bubble,
2046
- events = ['next', 'prev', 'start', 'end', 'show', 'error', 'close'],
2047
- eventPropName,
2048
- callbackProp,
2049
- i,
2050
- len;
2051
-
2052
- if (!opt) {
2053
- this.resetDefaultOptions();
2054
- }
2055
-
2056
- utils.extend(opt, options);
2057
-
2058
- if (options) {
2059
- utils.extend(customI18N, options.i18n);
2060
- }
2061
-
2062
- for (i = 0, len = events.length; i < len; ++i) {
2063
- // At this point, options[eventPropName] may have changed from an array
2064
- // to a function.
2065
- eventPropName = 'on' + events[i].charAt(0).toUpperCase() + events[i].substring(1);
2066
- if (options[eventPropName]) {
2067
- this.listen(events[i],
2068
- options[eventPropName],
2069
- isTourOptions);
2070
- }
2071
- }
2072
-
2073
- bubble = getBubble(true);
2074
-
2075
- return this;
2076
- };
2077
-
2078
- /**
2079
- * configure
2080
- *
2081
- * <pre>
2082
- * VALID OPTIONS INCLUDE...
2083
- *
2084
- * - bubbleWidth: Number - Default bubble width. Defaults to 280.
2085
- * - bubblePadding: Number - DEPRECATED. Default bubble padding. Defaults to 15.
2086
- * - smoothScroll: Boolean - should the page scroll smoothly to the next
2087
- * step? Defaults to TRUE.
2088
- * - scrollDuration: Number - Duration of page scroll. Only relevant when
2089
- * smoothScroll is set to true. Defaults to
2090
- * 1000ms.
2091
- * - scrollTopMargin: NUMBER - When the page scrolls, how much space should there
2092
- * be between the bubble/targetElement and the top
2093
- * of the viewport? Defaults to 200.
2094
- * - showCloseButton: Boolean - should the tour bubble show a close (X) button?
2095
- * Defaults to TRUE.
2096
- * - showPrevButton: Boolean - should the bubble have the Previous button?
2097
- * Defaults to FALSE.
2098
- * - showNextButton: Boolean - should the bubble have the Next button?
2099
- * Defaults to TRUE.
2100
- * - arrowWidth: Number - Default arrow width. (space between the bubble
2101
- * and the targetEl) Used for bubble position
2102
- * calculation. Only use this option if you are
2103
- * using your own custom CSS. Defaults to 20.
2104
- * - skipIfNoElement Boolean - If a specified target element is not found,
2105
- * should we skip to the next step? Defaults to
2106
- * TRUE.
2107
- * - onNext: Function - A callback to be invoked after every click on
2108
- * a "Next" button.
2109
- *
2110
- * - i18n: Object - For i18n purposes. Allows you to change the
2111
- * text of button labels and step numbers.
2112
- * - i18n.stepNums: Array\<String\> - Provide a list of strings to be shown as
2113
- * the step number, based on index of array. Unicode
2114
- * characters are supported. (e.g., ['&#x4e00;',
2115
- * '&#x4e8c;', '&#x4e09;']) If there are more steps
2116
- * than provided numbers, Arabic numerals
2117
- * ('4', '5', '6', etc.) will be used as default.
2118
- * - highlight: Boolean - Shows an overlay that highlights the selected element
2119
- * Defaults to FALSE.
2120
- * - highlightMargin: Number - Amount of margin around the selected element to show
2121
- * Defaults to 0
2122
- *
2123
- * // =========
2124
- * // CALLBACKS
2125
- * // =========
2126
- * - onNext: Function - Invoked after every click on a "Next" button.
2127
- * - onPrev: Function - Invoked after every click on a "Prev" button.
2128
- * - onStart: Function - Invoked when the tour is started.
2129
- * - onEnd: Function - Invoked when the tour ends.
2130
- * - onClose: Function - Invoked when the user closes the tour before finishing.
2131
- * - onError: Function - Invoked when the specified target element doesn't exist on the page.
2132
- *
2133
- * // ====
2134
- * // I18N
2135
- * // ====
2136
- * i18n: OBJECT - For i18n purposes. Allows you to change the text
2137
- * of button labels and step numbers.
2138
- * i18n.nextBtn: STRING - Label for next button
2139
- * i18n.prevBtn: STRING - Label for prev button
2140
- * i18n.doneBtn: STRING - Label for done button
2141
- * i18n.skipBtn: STRING - Label for skip button
2142
- * i18n.closeTooltip: STRING - Text for close button tooltip
2143
- * i18n.stepNums: ARRAY<STRING> - Provide a list of strings to be shown as
2144
- * the step number, based on index of array. Unicode
2145
- * characters are supported. (e.g., ['&#x4e00;',
2146
- * '&#x4e8c;', '&#x4e09;']) If there are more steps
2147
- * than provided numbers, Arabic numerals
2148
- * ('4', '5', '6', etc.) will be used as default.
2149
- * </pre>
2150
- *
2151
- * @example hopscotch.configure({ scrollDuration: 1000, scrollTopMargin: 150 });
2152
- * @example
2153
- * hopscotch.configure({
2154
- * scrollTopMargin: 150,
2155
- * onStart: function() {
2156
- * alert("Have fun!");
2157
- * },
2158
- * i18n: {
2159
- * nextBtn: 'Forward',
2160
- * prevBtn: 'Previous'
2161
- * closeTooltip: 'Quit'
2162
- * }
2163
- * });
2164
- *
2165
- * @param {Object} options A hash of configuration options.
2166
- * @returns {Object} Hopscotch
2167
- */
2168
- this.configure = function(options) {
2169
- return _configure.call(this, options, false);
2170
- };
2171
-
2172
- /**
2173
- * Set the template that should be used for rendering Hopscotch bubbles.
2174
- * If a string, it's assumed your template is available in the
2175
- * hopscotch.templates namespace.
2176
- *
2177
- * @param {String|Function(obj)} The template to use for rendering.
2178
- * @returns {Object} The Hopscotch object (for chaining).
2179
- */
2180
- this.setRenderer = function(render){
2181
- var typeOfRender = typeof render;
2182
-
2183
- if(typeOfRender === 'string'){
2184
- templateToUse = render;
2185
- customRenderer = undefined;
2186
- }
2187
- else if(typeOfRender === 'function'){
2188
- customRenderer = render;
2189
- }
2190
- return this;
2191
- };
2192
-
2193
- /**
2194
- * Sets the escaping method to be used by JST templates.
2195
- *
2196
- * @param {Function} - The escape method to use.
2197
- * @returns {Object} The Hopscotch object (for chaining).
2198
- */
2199
- this.setEscaper = function(esc){
2200
- if (typeof esc === 'function'){
2201
- customEscape = esc;
2202
- }
2203
- return this;
2204
- };
2205
-
2206
- init.call(this, initOptions);
2207
- };
2208
-
2209
-
2210
-
2211
- HopscotchHighlight = function(opt) {
2212
- this.init(opt);
2213
- };
2214
-
2215
- HopscotchHighlight.prototype = {
2216
- init: function(initOpt) {
2217
- var opt;
2218
- var el = {
2219
- top: document.createElement('div'),
2220
- left: document.createElement('div'),
2221
- right: document.createElement('div'),
2222
- bottom: document.createElement('div')
2223
- };
2224
-
2225
- this.element = el;
2226
-
2227
-
2228
- //Merge highlight options with defaults.
2229
- opt = {
2230
- highlight: defaultOpts.highlight,
2231
- highlightMargin: defaultOpts.highlightMargin
2232
- };
2233
-
2234
- initOpt = (typeof initOpt === undefinedStr ? {} : initOpt);
2235
- utils.extend(opt, initOpt);
2236
- this.opt = opt;
2237
-
2238
- for (var e in this.element){
2239
- utils.addClass(this.element[e], 'hopscotch-overlay');
2240
- }
2241
- },
2242
- addToDom: function(){
2243
- for (var e in this.element){
2244
- document.body.appendChild(this.element[e]);
2245
- }
2246
- },
2247
- show: function(){
2248
- // check if step has disabled the highlight:
2249
- if (!this.stepOpts.highlight){
2250
- return;
2251
- }
2252
-
2253
- for (var e in this.element){
2254
- utils.removeClass(this.element[e], 'hide');
2255
- }
2256
- },
2257
- hide: function(){
2258
- for (var e in this.element){
2259
- utils.addClass(this.element[e], 'hide');
2260
- }
2261
- },
2262
- setPosition: function(step, targetBounds){
2263
- // check if step has disabled the highlight:
2264
- if (!this.stepOpts.highlight){
2265
- return;
2266
- }
2267
-
2268
- var margin = this.stepOpts.highlightMargin;
2269
-
2270
- var body = document.body,
2271
- html = document.documentElement;
2272
-
2273
- var documentHeight = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight );
2274
-
2275
- // top div:
2276
- el = this.element.top;
2277
- el.style.top = '0px';
2278
- el.style.left = '0px';
2279
- el.style.width = window.screen.width + 'px';
2280
- el.style.height = targetBounds.top + utils.getScrollTop() - margin + 'px';
2281
-
2282
- // right div:
2283
- el = this.element.right;
2284
- el.style.top = targetBounds.top + utils.getScrollTop() - margin + 'px';
2285
- el.style.left = targetBounds.left + targetBounds.width + utils.getScrollLeft() + margin + 'px';
2286
- el.style.width = window.screen.width - (targetBounds.left + targetBounds.width + utils.getScrollLeft() + margin) + 'px';
2287
- el.style.height = targetBounds.height + margin * 2 + 'px';
2288
-
2289
- // bottom div:
2290
- el = this.element.bottom;
2291
- el.style.top = targetBounds.top + utils.getScrollTop() + targetBounds.height + margin + 'px';
2292
- el.style.left = '0px';
2293
- el.style.width = window.screen.width + 'px';
2294
- el.style.height = documentHeight - (targetBounds.top + utils.getScrollTop() + targetBounds.height + margin) + 'px';
2295
-
2296
- // left div:
2297
- el = this.element.left;
2298
- el.style.top = targetBounds.top + utils.getScrollTop() + - margin + 'px';
2299
- el.style.left = '0px';
2300
- el.style.width = targetBounds.left + utils.getScrollLeft() - margin + 'px';
2301
- el.style.height = targetBounds.height + margin * 2 + 'px';
2302
- },
2303
- render: function(step){
2304
- // set options for current step:
2305
- this.stepOpts = {};
2306
-
2307
- utils.extend(this.stepOpts, this.opt);
2308
- utils.extend(this.stepOpts, step);
2309
- }
2310
- };
2311
-
2312
-
2313
-
2314
- winHopscotch = new Hopscotch();
2315
- context[namespace] = winHopscotch;
2316
-
2317
- // Template includes, placed inside a closure to ensure we don't
2318
- // end up declaring our shim globally.
2319
- (function(){
2320
- var _ = {};
2321
- /*
2322
- * Adapted from the Underscore.js framework. Check it out at
2323
- * https://github.com/jashkenas/underscore
2324
- */
2325
- _.escape = function(str){
2326
- if(customEscape){ return customEscape(str); }
2327
-
2328
- if(str == null) return '';
2329
- return ('' + str).replace(new RegExp('[&<>"\']', 'g'), function(match){
2330
- if(match == '&'){ return '&amp;' }
2331
- if(match == '<'){ return '&lt;' }
2332
- if(match == '>'){ return '&gt;' }
2333
- if(match == '"'){ return '&quot;' }
2334
- if(match == "'"){ return '&#x27;' }
2335
- });
2336
- }
2337
- this["hopscotch"] = this["hopscotch"] || {};
2338
- this["hopscotch"]["templates"] = this["hopscotch"]["templates"] || {};
2339
-
2340
- this["hopscotch"]["templates"]["bubble_default"] = function(obj) {
2341
- obj || (obj = {});
2342
- var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
2343
- function print() { __p += __j.call(arguments, '') }
2344
- with (obj) {
2345
-
2346
-
2347
- function optEscape(str, unsafe){
2348
- if(unsafe){
2349
- return _.escape(str);
2350
- }
2351
- return str;
2352
- }
2353
- ;
2354
- __p += '\n<div class="hopscotch-bubble-container" style="width: ' +
2355
- ((__t = ( step.width )) == null ? '' : __t) +
2356
- 'px; padding: ' +
2357
- ((__t = ( step.padding )) == null ? '' : __t) +
2358
- 'px;">\n ';
2359
- if(tour.isTour){ ;
2360
- __p += '<span class="hopscotch-bubble-number">' +
2361
- ((__t = ( i18n.stepNum )) == null ? '' : __t) +
2362
- '</span>';
2363
- } ;
2364
- __p += '\n <div class="hopscotch-bubble-content">\n ';
2365
- if(step.title !== ''){ ;
2366
- __p += '<h3 class="hopscotch-title">' +
2367
- ((__t = ( optEscape(step.title, tour.unsafe) )) == null ? '' : __t) +
2368
- '</h3>';
2369
- } ;
2370
- __p += '\n ';
2371
- if(step.content !== ''){ ;
2372
- __p += '<div class="hopscotch-content">' +
2373
- ((__t = ( optEscape(step.content, tour.unsafe) )) == null ? '' : __t) +
2374
- '</div>';
2375
- } ;
2376
- __p += '\n </div>\n <div class="hopscotch-actions">\n ';
2377
- if(buttons.showPrev){ ;
2378
- __p += '<button class="hopscotch-nav-button prev hopscotch-prev">' +
2379
- ((__t = ( i18n.prevBtn )) == null ? '' : __t) +
2380
- '</button>';
2381
- } ;
2382
- __p += '\n ';
2383
- if(buttons.showCTA){ ;
2384
- __p += '<button class="hopscotch-nav-button next hopscotch-cta">' +
2385
- ((__t = ( buttons.ctaLabel )) == null ? '' : __t) +
2386
- '</button>';
2387
- } ;
2388
- __p += '\n ';
2389
- if(buttons.showNext){ ;
2390
- __p += '<button class="next hopscotch-next btn btn-primary btn-block btn-sm">' +
2391
- ((__t = ( i18n.nextBtn )) == null ? '' : __t) +
2392
- '</button>';
2393
- } ;
2394
- __p += '\n </div>\n ';
2395
- if(buttons.showClose){ ;
2396
- __p += '<a title="' +
2397
- ((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
2398
- '" href="#" class="hopscotch-bubble-close hopscotch-close">' +
2399
- ((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
2400
- '</a>';
2401
- } ;
2402
- __p += '\n</div>\n<div class="hopscotch-bubble-arrow-container hopscotch-arrow">\n <div class="hopscotch-bubble-arrow-border"></div>\n <div class="hopscotch-bubble-arrow"></div>\n</div>';
2403
-
2404
- }
2405
- return __p
2406
- };
2407
- }());
2408
-
2409
- }(window, 'hopscotch'));
1
+ (function(context, namespace) {
2
+ var Hopscotch,
3
+ HopscotchBubble,
4
+ HopscotchCalloutManager,
5
+ HopscotchI18N,
6
+ customI18N,
7
+ customRenderer,
8
+ customEscape,
9
+ templateToUse = 'bubble_default',
10
+ Sizzle = window.Sizzle || null,
11
+ utils,
12
+ callbacks,
13
+ helpers,
14
+ winLoadHandler,
15
+ defaultOpts,
16
+ winHopscotch = context[namespace],
17
+ undefinedStr = 'undefined',
18
+ waitingToStart = false, // is a tour waiting for the document to finish
19
+ // loading so that it can start?
20
+ hasJquery = (typeof window.jQuery !== undefinedStr),
21
+ hasSessionStorage = false,
22
+ document = window.document;
23
+
24
+ // If cookies are disabled, accessing sessionStorage can throw an error.
25
+ try {
26
+ hasSessionStorage = (typeof window.sessionStorage !== undefinedStr);
27
+ } catch (err) {}
28
+
29
+ defaultOpts = {
30
+ smoothScroll: true,
31
+ scrollDuration: 1000,
32
+ scrollTopMargin: 200,
33
+ showCloseButton: true,
34
+ showPrevButton: false,
35
+ showNextButton: true,
36
+ bubbleWidth: 280,
37
+ bubblePadding: 15,
38
+ arrowWidth: 20,
39
+ skipIfNoElement: true,
40
+ cookieName: 'hopscotch.tour.state',
41
+ highlight: false,
42
+ highlightMargin: 0
43
+ };
44
+
45
+ if (winHopscotch) {
46
+ // Hopscotch already exists.
47
+ return;
48
+ }
49
+
50
+ if (!Array.isArray) {
51
+ Array.isArray = function(obj) {
52
+ return Object.prototype.toString.call(obj) === '[object Array]';
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Called when the page is done loading.
58
+ *
59
+ * @private
60
+ */
61
+ winLoadHandler = function() {
62
+ if (waitingToStart) {
63
+ winHopscotch.startTour();
64
+ }
65
+ };
66
+
67
+ /**
68
+ * utils
69
+ * =====
70
+ * A set of utility functions, mostly for standardizing to manipulate
71
+ * and extract information from the DOM. Basically these are things I
72
+ * would normally use jQuery for, but I don't want to require it for
73
+ * this framework.
74
+ *
75
+ * @private
76
+ */
77
+ utils = {
78
+ /**
79
+ * addClass
80
+ * ========
81
+ * Adds one or more classes to a DOM element.
82
+ *
83
+ * @private
84
+ */
85
+ addClass: function(domEl, classToAdd) {
86
+ var domClasses,
87
+ classToAddArr,
88
+ setClass,
89
+ i,
90
+ len;
91
+
92
+ if (!domEl.className) {
93
+ domEl.className = classToAdd;
94
+ }
95
+ else {
96
+ classToAddArr = classToAdd.split(/\s+/);
97
+ domClasses = ' ' + domEl.className + ' ';
98
+ for (i = 0, len = classToAddArr.length; i < len; ++i) {
99
+ if (domClasses.indexOf(' ' + classToAddArr[i] + ' ') < 0) {
100
+ domClasses += classToAddArr[i] + ' ';
101
+ }
102
+ }
103
+ domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
104
+ }
105
+ },
106
+
107
+ /**
108
+ * removeClass
109
+ * ===========
110
+ * Remove one or more classes from a DOM element.
111
+ *
112
+ * @private
113
+ */
114
+ removeClass: function(domEl, classToRemove) {
115
+ var domClasses,
116
+ classToRemoveArr,
117
+ currClass,
118
+ i,
119
+ len;
120
+
121
+ classToRemoveArr = classToRemove.split(/\s+/);
122
+ domClasses = ' ' + domEl.className + ' ';
123
+ for (i = 0, len = classToRemoveArr.length; i < len; ++i) {
124
+ domClasses = domClasses.replace(' ' + classToRemoveArr[i] + ' ', ' ');
125
+ }
126
+ domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
127
+ },
128
+
129
+ /**
130
+ * hasClass
131
+ * ========
132
+ * Determine if a given DOM element has a class.
133
+ */
134
+ hasClass: function(domEl, classToCheck){
135
+ var classes;
136
+
137
+ if(!domEl.className){ return false; }
138
+ classes = ' ' + domEl.className + ' ';
139
+ return (classes.indexOf(' ' + classToCheck + ' ') !== -1);
140
+ },
141
+
142
+ /**
143
+ * @private
144
+ */
145
+ getPixelValue: function(val) {
146
+ var valType = typeof val;
147
+ if (valType === 'number') { return val; }
148
+ if (valType === 'string') { return parseInt(val, 10); }
149
+ return 0;
150
+ },
151
+
152
+ /**
153
+ * Inspired by Python... returns val if it's defined, otherwise returns the default.
154
+ *
155
+ * @private
156
+ */
157
+ valOrDefault: function(val, valDefault) {
158
+ return typeof val !== undefinedStr ? val : valDefault;
159
+ },
160
+
161
+ /**
162
+ * Invokes a single callback represented by an array.
163
+ * Example input: ["my_fn", "arg1", 2, "arg3"]
164
+ * @private
165
+ */
166
+ invokeCallbackArrayHelper: function(arr) {
167
+ // Logic for a single callback
168
+ var fn;
169
+ if (Array.isArray(arr)) {
170
+ fn = helpers[arr[0]];
171
+ if (typeof fn === 'function') {
172
+ return fn.apply(this, arr.slice(1));
173
+ }
174
+ }
175
+ },
176
+
177
+ /**
178
+ * Invokes one or more callbacks. Array should have at most one level of nesting.
179
+ * Example input:
180
+ * ["my_fn", "arg1", 2, "arg3"]
181
+ * [["my_fn_1", "arg1", "arg2"], ["my_fn_2", "arg2-1", "arg2-2"]]
182
+ * [["my_fn_1", "arg1", "arg2"], function() { ... }]
183
+ * @private
184
+ */
185
+ invokeCallbackArray: function(arr) {
186
+ var i, len;
187
+
188
+ if (Array.isArray(arr)) {
189
+ if (typeof arr[0] === 'string') {
190
+ // Assume there are no nested arrays. This is the one and only callback.
191
+ return utils.invokeCallbackArrayHelper(arr);
192
+ }
193
+ else { // assume an array
194
+ for (i = 0, len = arr.length; i < len; ++i) {
195
+ utils.invokeCallback(arr[i]);
196
+ }
197
+ }
198
+ }
199
+ },
200
+
201
+ /**
202
+ * Helper function for invoking a callback, whether defined as a function literal
203
+ * or an array that references a registered helper function.
204
+ * @private
205
+ */
206
+ invokeCallback: function(cb) {
207
+ if (typeof cb === 'function') {
208
+ return cb();
209
+ }
210
+ if (typeof cb === 'string' && helpers[cb]) { // name of a helper
211
+ return helpers[cb]();
212
+ }
213
+ else { // assuming array
214
+ return utils.invokeCallbackArray(cb);
215
+ }
216
+ },
217
+
218
+ /**
219
+ * If stepCb (the step-specific helper callback) is passed in, then invoke
220
+ * it first. Then invoke tour-wide helper.
221
+ *
222
+ * @private
223
+ */
224
+ invokeEventCallbacks: function(evtType, stepCb) {
225
+ var cbArr = callbacks[evtType],
226
+ callback,
227
+ fn,
228
+ i,
229
+ len;
230
+
231
+ if (stepCb) {
232
+ return this.invokeCallback(stepCb);
233
+ }
234
+
235
+ for (i=0, len=cbArr.length; i<len; ++i) {
236
+ this.invokeCallback(cbArr[i].cb);
237
+ }
238
+ },
239
+
240
+ /**
241
+ * @private
242
+ */
243
+ getScrollTop: function() {
244
+ var scrollTop;
245
+ if (typeof window.pageYOffset !== undefinedStr) {
246
+ scrollTop = window.pageYOffset;
247
+ }
248
+ else {
249
+ // Most likely IE <=8, which doesn't support pageYOffset
250
+ scrollTop = document.documentElement.scrollTop;
251
+ }
252
+ return scrollTop;
253
+ },
254
+
255
+ /**
256
+ * @private
257
+ */
258
+ getScrollLeft: function() {
259
+ var scrollLeft;
260
+ if (typeof window.pageXOffset !== undefinedStr) {
261
+ scrollLeft = window.pageXOffset;
262
+ }
263
+ else {
264
+ // Most likely IE <=8, which doesn't support pageXOffset
265
+ scrollLeft = document.documentElement.scrollLeft;
266
+ }
267
+ return scrollLeft;
268
+ },
269
+
270
+ /**
271
+ * @private
272
+ */
273
+ getWindowHeight: function() {
274
+ return window.innerHeight || document.documentElement.clientHeight;
275
+ },
276
+
277
+ /**
278
+ * @private
279
+ */
280
+ getWindowWidth: function() {
281
+ return window.innerWidth || document.documentElement.clientWidth;
282
+ },
283
+
284
+ /**
285
+ * @private
286
+ */
287
+ addEvtListener: function(el, evtName, fn) {
288
+ return el.addEventListener ? el.addEventListener(evtName, fn, false) : el.attachEvent('on' + evtName, fn);
289
+ },
290
+
291
+ /**
292
+ * @private
293
+ */
294
+ removeEvtListener: function(el, evtName, fn) {
295
+ return el.removeEventListener ? el.removeEventListener(evtName, fn, false) : el.detachEvent('on' + evtName, fn);
296
+ },
297
+
298
+ documentIsReady: function() {
299
+ return document.readyState === 'complete' || document.readyState === 'interactive';
300
+ },
301
+
302
+ /**
303
+ * @private
304
+ */
305
+ evtPreventDefault: function(evt) {
306
+ if (evt.preventDefault) {
307
+ evt.preventDefault();
308
+ }
309
+ else if (event) {
310
+ event.returnValue = false;
311
+ }
312
+ },
313
+
314
+ /**
315
+ * @private
316
+ */
317
+ extend: function(obj1, obj2) {
318
+ var prop;
319
+ for (prop in obj2) {
320
+ if (obj2.hasOwnProperty(prop)) {
321
+ obj1[prop] = obj2[prop];
322
+ }
323
+ }
324
+ },
325
+
326
+ /**
327
+ * Helper function to get a single target DOM element. We will try to
328
+ * locate the DOM element through several ways, in the following order:
329
+ *
330
+ * 1) Passing the string into document.querySelector
331
+ * 2) Passing the string to jQuery, if it exists
332
+ * 3) Passing the string to Sizzle, if it exists
333
+ * 4) Calling document.getElementById if it is a plain id
334
+ *
335
+ * Default case is to assume the string is a plain id and call
336
+ * document.getElementById on it.
337
+ *
338
+ * @private
339
+ */
340
+ getStepTargetHelper: function(target){
341
+ var result = document.getElementById(target);
342
+
343
+ //Backwards compatibility: assume the string is an id
344
+ if (result) {
345
+ return result;
346
+ }
347
+ if (document.querySelector) {
348
+ return document.querySelector(target);
349
+ }
350
+ if (hasJquery) {
351
+ result = jQuery(target);
352
+ return result.length ? result[0] : null;
353
+ }
354
+ if (Sizzle) {
355
+ result = new Sizzle(target);
356
+ return result.length ? result[0] : null;
357
+ }
358
+ // Regex test for id. Following the HTML 4 spec for valid id formats.
359
+ // (http://www.w3.org/TR/html4/types.html#type-id)
360
+ if (/^#[a-zA-Z][\w-_:.]*$/.test(target)) {
361
+ return document.getElementById(target.substring(1));
362
+ }
363
+
364
+ return null;
365
+ },
366
+
367
+ /**
368
+ * Given a step, returns the target DOM element associated with it. It is
369
+ * recommended to only assign one target per step. However, there are
370
+ * some use cases which require multiple step targets to be supplied. In
371
+ * this event, we will use the first target in the array that we can
372
+ * locate on the page. See the comments for getStepTargetHelper for more
373
+ * information.
374
+ *
375
+ * @private
376
+ */
377
+ getStepTarget: function(step) {
378
+ var queriedTarget;
379
+
380
+ if (!step || !step.target) {
381
+ return null;
382
+ }
383
+
384
+ if (typeof step.target === 'string') {
385
+ //Just one target to test. Check, cache, and return its results.
386
+ step.target = utils.getStepTargetHelper(step.target);
387
+ return step.target;
388
+ }
389
+ else if (Array.isArray(step.target)) {
390
+ // Multiple items to check. Check each and return the first success.
391
+ // Assuming they are all strings.
392
+ var i,
393
+ len;
394
+
395
+ for (i = 0, len = step.target.length; i < len; i++){
396
+ if (typeof step.target[i] === 'string') {
397
+ queriedTarget = utils.getStepTargetHelper(step.target[i]);
398
+
399
+ if (queriedTarget) {
400
+ // Replace step.target with result so we don't have to look it up again.
401
+ step.target = queriedTarget;
402
+ return queriedTarget;
403
+ }
404
+ }
405
+ }
406
+ return null;
407
+ }
408
+
409
+ // Assume that the step.target is a DOM element
410
+ return step.target;
411
+ },
412
+
413
+ /**
414
+ * Convenience method for getting an i18n string. Returns custom i18n value
415
+ * or the default i18n value if no custom value exists.
416
+ *
417
+ * @private
418
+ */
419
+ getI18NString: function(key) {
420
+ return customI18N[key] || HopscotchI18N[key];
421
+ },
422
+
423
+ // Tour session persistence for multi-page tours. Uses HTML5 sessionStorage if available, then
424
+ // falls back to using cookies.
425
+ //
426
+ // The following cookie-related logic is borrowed from:
427
+ // http://www.quirksmode.org/js/cookies.html
428
+
429
+ /**
430
+ * @private
431
+ */
432
+ setState: function(name,value,days) {
433
+ var expires = '',
434
+ date;
435
+
436
+ if (hasSessionStorage) {
437
+ sessionStorage.setItem(name, value);
438
+ }
439
+ else {
440
+ if (days) {
441
+ date = new Date();
442
+ date.setTime(date.getTime()+(days*24*60*60*1000));
443
+ expires = '; expires='+date.toGMTString();
444
+ }
445
+ document.cookie = name+'='+value+expires+'; path=/';
446
+ }
447
+ },
448
+
449
+ /**
450
+ * @private
451
+ */
452
+ getState: function(name) {
453
+ var nameEQ = name + '=',
454
+ ca = document.cookie.split(';'),
455
+ i,
456
+ c,
457
+ state;
458
+
459
+ if (hasSessionStorage) {
460
+ state = sessionStorage.getItem(name);
461
+ }
462
+ else {
463
+ for(i=0;i < ca.length;i++) {
464
+ c = ca[i];
465
+ while (c.charAt(0)===' ') {c = c.substring(1,c.length);}
466
+ if (c.indexOf(nameEQ) === 0) {
467
+ state = c.substring(nameEQ.length,c.length);
468
+ break;
469
+ }
470
+ }
471
+ }
472
+
473
+ return state;
474
+ },
475
+
476
+ /**
477
+ * @private
478
+ */
479
+ clearState: function(name) {
480
+ if (hasSessionStorage) {
481
+ sessionStorage.removeItem(name);
482
+ }
483
+ else {
484
+ this.setState(name,'',-1);
485
+ }
486
+ }
487
+ };
488
+
489
+ utils.addEvtListener(window, 'load', winLoadHandler);
490
+
491
+ callbacks = {
492
+ next: [],
493
+ prev: [],
494
+ start: [],
495
+ end: [],
496
+ show: [],
497
+ error: [],
498
+ close: []
499
+ };
500
+
501
+ /**
502
+ * helpers
503
+ * =======
504
+ * A map of functions to be used as callback listeners. Functions are
505
+ * added to and removed from the map using the functions
506
+ * Hopscotch.registerHelper() and Hopscotch.unregisterHelper().
507
+ */
508
+ helpers = {};
509
+
510
+ HopscotchI18N = {
511
+ stepNums: null,
512
+ nextBtn: 'Next',
513
+ prevBtn: 'Back',
514
+ doneBtn: 'Done',
515
+ skipBtn: 'Skip',
516
+ closeTooltip: 'Close'
517
+ };
518
+
519
+ customI18N = {}; // Developer's custom i18n strings goes here.
520
+
521
+ /**
522
+ * HopscotchBubble
523
+ *
524
+ * @class The HopscotchBubble class represents the view of a bubble. This class is also used for Hopscotch callouts.
525
+ */
526
+ HopscotchBubble = function(opt) {
527
+ this.init(opt);
528
+ };
529
+
530
+ HopscotchBubble.prototype = {
531
+ isShowing: false,
532
+
533
+ currStep: undefined,
534
+
535
+ /**
536
+ * setPosition
537
+ *
538
+ * Sets the position of the bubble using the bounding rectangle of the
539
+ * target element and the orientation and offset information specified by
540
+ * the JSON.
541
+ */
542
+ setPosition: function(step) {
543
+ var bubbleBoundingHeight,
544
+ bubbleBoundingWidth,
545
+ boundingRect,
546
+ top,
547
+ left,
548
+ arrowOffset,
549
+ self = this,
550
+ targetEl = utils.getStepTarget(step),
551
+ el = this.element,
552
+ arrowEl = this.arrowEl;
553
+
554
+ bubbleBoundingWidth = el.offsetWidth;
555
+ bubbleBoundingHeight = el.offsetHeight;
556
+ utils.removeClass(el, 'fade-in-down fade-in-up fade-in-left fade-in-right');
557
+
558
+ // Originally called it orientation, but placement is more intuitive.
559
+ // Allowing both for now for backwards compatibility.
560
+ if (!step.placement && step.orientation) {
561
+ step.placement = step.orientation;
562
+ }
563
+
564
+ // SET POSITION
565
+ boundingRect = targetEl.getBoundingClientRect();
566
+ if (step.placement === 'top') {
567
+ top = (boundingRect.top - bubbleBoundingHeight) - this.opt.arrowWidth;
568
+ left = boundingRect.left;
569
+ }
570
+ else if (step.placement === 'bottom') {
571
+ top = boundingRect.bottom + this.opt.arrowWidth;
572
+ left = boundingRect.left;
573
+ }
574
+ else if (step.placement === 'left') {
575
+ top = boundingRect.top;
576
+ left = boundingRect.left - bubbleBoundingWidth - this.opt.arrowWidth;
577
+ }
578
+ else if (step.placement === 'right') {
579
+ top = boundingRect.top;
580
+ left = boundingRect.right + this.opt.arrowWidth;
581
+ }
582
+ else {
583
+ throw 'Bubble placement failed because step.placement is invalid or undefined!';
584
+ }
585
+
586
+ // UPDATE HIGHLIGHT
587
+ self.highlight.setPosition(step, boundingRect);
588
+
589
+
590
+ // SET (OR RESET) ARROW OFFSETS
591
+ if (step.arrowOffset !== 'center') {
592
+ arrowOffset = utils.getPixelValue(step.arrowOffset);
593
+ }
594
+ else {
595
+ arrowOffset = step.arrowOffset;
596
+ }
597
+ if (!arrowOffset) {
598
+ arrowEl.style.top = '';
599
+ arrowEl.style.left = '';
600
+ }
601
+ else if (step.placement === 'top' || step.placement === 'bottom') {
602
+ arrowEl.style.top = '';
603
+ if (arrowOffset === 'center') {
604
+ arrowEl.style.left = Math.floor((bubbleBoundingWidth / 2) - arrowEl.offsetWidth/2) + 'px';
605
+ }
606
+ else {
607
+ // Numeric pixel value
608
+ arrowEl.style.left = arrowOffset + 'px';
609
+ }
610
+ }
611
+ else if (step.placement === 'left' || step.placement === 'right') {
612
+ arrowEl.style.left = '';
613
+ if (arrowOffset === 'center') {
614
+ arrowEl.style.top = Math.floor((bubbleBoundingHeight / 2) - arrowEl.offsetHeight/2) + 'px';
615
+ }
616
+ else {
617
+ // Numeric pixel value
618
+ arrowEl.style.top = arrowOffset + 'px';
619
+ }
620
+ }
621
+
622
+ // HORIZONTAL OFFSET
623
+ if (step.xOffset === 'center') {
624
+ left = (boundingRect.left + targetEl.offsetWidth/2) - (bubbleBoundingWidth / 2);
625
+ }
626
+ else {
627
+ left += utils.getPixelValue(step.xOffset);
628
+ }
629
+ // VERTICAL OFFSET
630
+ if (step.yOffset === 'center') {
631
+ top = (boundingRect.top + targetEl.offsetHeight/2) - (bubbleBoundingHeight / 2);
632
+ }
633
+ else {
634
+ top += utils.getPixelValue(step.yOffset);
635
+ }
636
+
637
+ // ADJUST TOP FOR SCROLL POSITION
638
+ if (!step.fixedElement) {
639
+ top += utils.getScrollTop();
640
+ left += utils.getScrollLeft();
641
+ }
642
+
643
+ // ACCOUNT FOR FIXED POSITION ELEMENTS
644
+ el.style.position = (step.fixedElement ? 'fixed' : 'absolute');
645
+
646
+ el.style.top = top + 'px';
647
+ el.style.left = left + 'px';
648
+ },
649
+
650
+ /**
651
+ * Renders the bubble according to the step JSON.
652
+ *
653
+ * @param {Object} step Information defining how the bubble should look.
654
+ * @param {Number} idx The index of the step in the tour. Not used for callouts.
655
+ * @param {Function} callback Function to be invoked after rendering is finished.
656
+ */
657
+ render: function(step, idx, callback) {
658
+ var el = this.element,
659
+ tourSpecificRenderer,
660
+ customTourData,
661
+ unsafe,
662
+ currTour,
663
+ totalSteps,
664
+ nextBtnText,
665
+ isLast,
666
+ opts;
667
+
668
+ // Cache current step information.
669
+ if (step) {
670
+ this.currStep = step;
671
+ }
672
+ else if (this.currStep) {
673
+ step = this.currStep;
674
+ }
675
+
676
+ // update highlight with current step information
677
+ this.highlight.render(step);
678
+
679
+ // Check current tour for total number of steps and custom render data
680
+ if(this.opt.isTourBubble){
681
+ currTour = winHopscotch.getCurrTour();
682
+ if(currTour){
683
+ customTourData = currTour.customData;
684
+ tourSpecificRenderer = currTour.customRenderer;
685
+ unsafe = currTour.unsafe;
686
+ if(Array.isArray(currTour.steps)){
687
+ totalSteps = currTour.steps.length;
688
+ isLast = (idx === totalSteps - 1);
689
+ }
690
+ }
691
+ }else{
692
+ customTourData = step.customData;
693
+ tourSpecificRenderer = step.customRenderer;
694
+ unsafe = step.unsafe;
695
+ }
696
+
697
+ // Determine label for next button
698
+ if(isLast){
699
+ nextBtnText = utils.getI18NString('doneBtn');
700
+ } else if(step.showSkip) {
701
+ nextBtnText = utils.getI18NString('skipBtn');
702
+ } else {
703
+ nextBtnText = utils.getI18NString('nextBtn');
704
+ }
705
+
706
+ // Originally called it orientation, but placement is more intuitive.
707
+ // Allowing both for now for backwards compatibility.
708
+ if (!step.placement && step.orientation) {
709
+ step.placement = step.orientation;
710
+ }
711
+ this.placement = step.placement;
712
+
713
+ // Setup the configuration options we want to pass along to the template
714
+ opts = {
715
+ i18n: {
716
+ prevBtn: utils.getI18NString('prevBtn'),
717
+ nextBtn: nextBtnText,
718
+ closeTooltip: utils.getI18NString('closeTooltip'),
719
+ stepNum: this._getStepI18nNum(idx),
720
+ },
721
+ buttons:{
722
+ showPrev: (utils.valOrDefault(step.showPrevButton, this.opt.showPrevButton) && (idx > 0)),
723
+ showNext: utils.valOrDefault(step.showNextButton, this.opt.showNextButton),
724
+ showCTA: utils.valOrDefault((step.showCTAButton && step.ctaLabel), false),
725
+ ctaLabel: step.ctaLabel,
726
+ showClose: utils.valOrDefault(this.opt.showCloseButton, true)
727
+ },
728
+ step:{
729
+ num: idx,
730
+ isLast: utils.valOrDefault(isLast, false),
731
+ title: (step.title || ''),
732
+ content: (step.content || ''),
733
+ placement: step.placement,
734
+ padding: utils.valOrDefault(step.padding, this.opt.bubblePadding),
735
+ width: utils.getPixelValue(step.width) || this.opt.bubbleWidth,
736
+ customData: (step.customData || {})
737
+ },
738
+ tour:{
739
+ isTour: this.opt.isTourBubble,
740
+ numSteps: totalSteps,
741
+ unsafe: utils.valOrDefault(unsafe, false),
742
+ customData: (customTourData || {})
743
+ }
744
+ };
745
+
746
+ // Render the bubble's content.
747
+ // Use tour renderer if available, then the global customRenderer if defined.
748
+ if(typeof tourSpecificRenderer === 'function'){
749
+ el.innerHTML = tourSpecificRenderer(opts);
750
+ }
751
+ else if(typeof tourSpecificRenderer === 'string'){
752
+ if(!hopscotch.templates || (typeof hopscotch.templates[tourSpecificRenderer] !== 'function')){
753
+ throw 'Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.';
754
+ }
755
+ el.innerHTML = hopscotch.templates[tourSpecificRenderer](opts);
756
+ }
757
+ else if(customRenderer){
758
+ el.innerHTML = customRenderer(opts);
759
+ }
760
+ else{
761
+ if(!hopscotch.templates || (typeof hopscotch.templates[templateToUse] !== 'function')){
762
+ throw 'Bubble rendering failed - template "' + templateToUse + '" is not a function.';
763
+ }
764
+ el.innerHTML = hopscotch.templates[templateToUse](opts);
765
+ }
766
+
767
+ // Find arrow among new child elements.
768
+ children = el.children;
769
+ numChildren = children.length;
770
+ for (i = 0; i < numChildren; i++){
771
+ node = children[i];
772
+
773
+ if(utils.hasClass(node, 'hopscotch-arrow')){
774
+ this.arrowEl = node;
775
+ }
776
+ }
777
+
778
+ // Set z-index and arrow placement
779
+ el.style.zIndex = step.zindex || '';
780
+ this._setArrow(step.placement);
781
+
782
+ // Set bubble positioning
783
+ // Make sure we're using visibility:hidden instead of display:none for height/width calculations.
784
+ this.hide(false);
785
+ this.setPosition(step);
786
+
787
+ // only want to adjust window scroll for non-fixed elements
788
+ if (callback) {
789
+ callback(!step.fixedElement);
790
+ }
791
+
792
+ return this;
793
+ },
794
+
795
+ /**
796
+ * Get the I18N step number for the current step.
797
+ *
798
+ * @private
799
+ */
800
+ _getStepI18nNum: function(idx) {
801
+ var stepNumI18N = utils.getI18NString('stepNums');
802
+ if (stepNumI18N && idx < stepNumI18N.length) {
803
+ idx = stepNumI18N[idx];
804
+ }
805
+ else {
806
+ idx = idx + 1;
807
+ }
808
+ return idx;
809
+ },
810
+
811
+ /**
812
+ * Sets which side the arrow is on.
813
+ *
814
+ * @private
815
+ */
816
+ _setArrow: function(orientation) {
817
+ utils.removeClass(this.arrowEl, 'down up right left');
818
+
819
+ // Whatever the orientation is, we want to arrow to appear
820
+ // "opposite" of the orientation. E.g., a top orientation
821
+ // requires a bottom arrow.
822
+ if (orientation === 'top') {
823
+ utils.addClass(this.arrowEl, 'down');
824
+ }
825
+ else if (orientation === 'bottom') {
826
+ utils.addClass(this.arrowEl, 'up');
827
+ }
828
+ else if (orientation === 'left') {
829
+ utils.addClass(this.arrowEl, 'right');
830
+ }
831
+ else if (orientation === 'right') {
832
+ utils.addClass(this.arrowEl, 'left');
833
+ }
834
+ },
835
+
836
+ /**
837
+ * @private
838
+ */
839
+ _getArrowDirection: function() {
840
+ if (this.placement === 'top') {
841
+ return 'down';
842
+ }
843
+ if (this.placement === 'bottom') {
844
+ return 'up';
845
+ }
846
+ if (this.placement === 'left') {
847
+ return 'right';
848
+ }
849
+ if (this.placement === 'right') {
850
+ return 'left';
851
+ }
852
+ },
853
+
854
+ show: function() {
855
+ var self = this,
856
+ fadeClass = 'fade-in-' + this._getArrowDirection(),
857
+ fadeDur = 1000;
858
+
859
+ utils.removeClass(this.element, 'hide');
860
+ utils.addClass(this.element, fadeClass);
861
+ setTimeout(function() {
862
+ utils.removeClass(self.element, 'invisible');
863
+ }, 50);
864
+ setTimeout(function() {
865
+ utils.removeClass(self.element, fadeClass);
866
+ }, fadeDur);
867
+ this.isShowing = true;
868
+ this.highlight.show();
869
+
870
+ return this;
871
+ },
872
+
873
+ hide: function(remove) {
874
+ var el = this.element;
875
+
876
+ remove = utils.valOrDefault(remove, true);
877
+ el.style.top = '';
878
+ el.style.left = '';
879
+
880
+ // display: none
881
+ if (remove) {
882
+ utils.addClass(el, 'hide');
883
+ utils.removeClass(el, 'invisible');
884
+ }
885
+ // opacity: 0
886
+ else {
887
+ utils.removeClass(el, 'hide');
888
+ utils.addClass(el, 'invisible');
889
+ }
890
+ utils.removeClass(el, 'animate fade-in-up fade-in-down fade-in-right fade-in-left');
891
+ this.isShowing = false;
892
+ this.highlight.hide();
893
+
894
+ return this;
895
+ },
896
+
897
+ destroy: function() {
898
+ var el = this.element;
899
+
900
+ if (el) {
901
+ el.parentNode.removeChild(el);
902
+ }
903
+ utils.removeEvtListener(el, 'click', this.clickCb);
904
+ },
905
+
906
+ _handleBubbleClick: function(evt){
907
+ var action;
908
+
909
+ //Recursively look up the parent tree until we find a match
910
+ //with one of the classes we're looking for, or the triggering element.
911
+ function findMatchRecur(el){
912
+ /* We're going to make the assumption that we're not binding
913
+ * multiple event classes to the same element.
914
+ * (next + previous = wait... err... what?)
915
+ *
916
+ * In the odd event we end up with an element with multiple
917
+ * possible matches, the following priority order is applied:
918
+ * hopscotch-cta, hopscotch-next, hopscotch-prev, hopscotch-close
919
+ */
920
+ if(el === evt.currentTarget){ return null; }
921
+ if(utils.hasClass(el, 'hopscotch-cta')){ return 'cta'; }
922
+ if(utils.hasClass(el, 'hopscotch-next')){ return 'next'; }
923
+ if(utils.hasClass(el, 'hopscotch-prev')){ return 'prev'; }
924
+ if(utils.hasClass(el, 'hopscotch-close')){ return 'close'; }
925
+ /*else*/ return findMatchRecur(el.parentElement);
926
+ }
927
+
928
+ action = findMatchRecur(evt.target);
929
+
930
+ //Now that we know what action we should take, let's take it.
931
+ if (action === 'cta'){
932
+ if (!this.opt.isTourBubble) {
933
+ // This is a callout. Close the callout when CTA is clicked.
934
+ winHopscotch.getCalloutManager().removeCallout(this.currStep.id);
935
+ }
936
+ // Call onCTA callback if one is provided
937
+ if (this.currStep.onCTA) {
938
+ utils.invokeCallback(this.currStep.onCTA);
939
+ }
940
+ }
941
+ else if (action === 'next'){
942
+ winHopscotch.nextStep(true);
943
+ }
944
+ else if (action === 'prev'){
945
+ winHopscotch.prevStep(true);
946
+ }
947
+ else if (action === 'close'){
948
+ if (this.opt.isTourBubble){
949
+ var currStepNum = winHopscotch.getCurrStepNum(),
950
+ currTour = winHopscotch.getCurrTour(),
951
+ doEndCallback = (currStepNum === currTour.steps.length-1);
952
+
953
+ utils.invokeEventCallbacks('close');
954
+
955
+ winHopscotch.endTour(true, doEndCallback);
956
+ } else {
957
+ if (this.opt.onClose) {
958
+ utils.invokeCallback(this.opt.onClose);
959
+ }
960
+ if (this.opt.id && !this.opt.isTourBubble) {
961
+ // Remove via the HopscotchCalloutManager.
962
+ // removeCallout() calls HopscotchBubble.destroy internally.
963
+ winHopscotch.getCalloutManager().removeCallout(this.opt.id);
964
+ }
965
+ else {
966
+ this.destroy();
967
+ }
968
+ }
969
+
970
+ utils.evtPreventDefault(evt);
971
+ }
972
+ //Otherwise, do nothing. We didn't click on anything relevant.
973
+ },
974
+
975
+ init: function(initOpt) {
976
+ var el = document.createElement('div'),
977
+ self = this,
978
+ resizeCooldown = false, // for updating after window resize
979
+ onWinResize,
980
+ appendToBody,
981
+ children,
982
+ numChildren,
983
+ node,
984
+ i,
985
+ opt;
986
+
987
+ //Register DOM element for this bubble.
988
+ this.element = el;
989
+
990
+ //Merge bubble options with defaults.
991
+ opt = {
992
+ showPrevButton: defaultOpts.showPrevButton,
993
+ showNextButton: defaultOpts.showNextButton,
994
+ bubbleWidth: defaultOpts.bubbleWidth,
995
+ bubblePadding: defaultOpts.bubblePadding,
996
+ arrowWidth: defaultOpts.arrowWidth,
997
+ showNumber: true,
998
+ isTourBubble: true
999
+ };
1000
+ initOpt = (typeof initOpt === undefinedStr ? {} : initOpt);
1001
+ utils.extend(opt, initOpt);
1002
+ this.opt = opt;
1003
+
1004
+ //Apply classes to bubble. Add "animated" for fade css animation
1005
+ el.className = 'hopscotch-bubble animated';
1006
+ if (!opt.isTourBubble) {
1007
+ utils.addClass(el, 'hopscotch-callout no-number');
1008
+ }
1009
+
1010
+ self.highlight = new HopscotchHighlight(initOpt);
1011
+
1012
+ /**
1013
+ * Not pretty, but IE8 doesn't support Function.bind(), so I'm
1014
+ * relying on closures to keep a handle of "this".
1015
+ * Reset position of bubble when window is resized
1016
+ *
1017
+ * @private
1018
+ */
1019
+ onWinResize = function() {
1020
+ if (resizeCooldown || !self.isShowing) {
1021
+ return;
1022
+ }
1023
+
1024
+ resizeCooldown = true;
1025
+ setTimeout(function() {
1026
+ self.setPosition(self.currStep);
1027
+ resizeCooldown = false;
1028
+ }, 100);
1029
+ };
1030
+
1031
+ //Add listener to reset bubble position on window resize
1032
+ utils.addEvtListener(window, 'resize', onWinResize);
1033
+
1034
+ //Create our click callback handler and keep a
1035
+ //reference to it for later.
1036
+ this.clickCb = function(evt){
1037
+ self._handleBubbleClick(evt);
1038
+ };
1039
+ utils.addEvtListener(el, 'click', this.clickCb);
1040
+
1041
+ //Hide the bubble by default
1042
+ this.hide();
1043
+
1044
+ //Finally, append our new bubble to body once the DOM is ready.
1045
+ if (utils.documentIsReady()) {
1046
+ document.body.appendChild(el);
1047
+ self.highlight.addToDom();
1048
+ }
1049
+ else {
1050
+ // Moz, webkit, Opera
1051
+ if (document.addEventListener) {
1052
+ appendToBody = function() {
1053
+ document.removeEventListener('DOMContentLoaded', appendToBody);
1054
+ window.removeEventListener('load', appendToBody);
1055
+
1056
+ document.body.appendChild(el);
1057
+ self.highlight.addToDom();
1058
+ };
1059
+
1060
+ document.addEventListener('DOMContentLoaded', appendToBody, false);
1061
+ }
1062
+ // IE
1063
+ else {
1064
+ appendToBody = function() {
1065
+ if (document.readyState === 'complete') {
1066
+ document.detachEvent('onreadystatechange', appendToBody);
1067
+ window.detachEvent('onload', appendToBody);
1068
+ document.body.appendChild(el);
1069
+ self.highlight.addToDom();
1070
+ }
1071
+ };
1072
+
1073
+ document.attachEvent('onreadystatechange', appendToBody);
1074
+ }
1075
+ utils.addEvtListener(window, 'load', appendToBody);
1076
+ }
1077
+ }
1078
+ };
1079
+
1080
+ /**
1081
+ * HopscotchCalloutManager
1082
+ *
1083
+ * @class Manages the creation and destruction of single callouts.
1084
+ * @constructor
1085
+ */
1086
+ HopscotchCalloutManager = function() {
1087
+ var callouts = {};
1088
+
1089
+ /**
1090
+ * createCallout
1091
+ *
1092
+ * Creates a standalone callout. This callout has the same API
1093
+ * as a Hopscotch tour bubble.
1094
+ *
1095
+ * @param {Object} opt The options for the callout. For the most
1096
+ * part, these are the same options as you would find in a tour
1097
+ * step.
1098
+ */
1099
+ this.createCallout = function(opt) {
1100
+ var callout;
1101
+
1102
+ if (opt.id) {
1103
+ if (callouts[opt.id]) {
1104
+ throw 'Callout by that id already exists. Please choose a unique id.';
1105
+ }
1106
+ opt.showNextButton = opt.showPrevButton = false;
1107
+ opt.isTourBubble = false;
1108
+ callout = new HopscotchBubble(opt);
1109
+ callouts[opt.id] = callout;
1110
+ if (opt.target) {
1111
+ callout.render(opt, null, function() {
1112
+ callout.show();
1113
+ });
1114
+ }
1115
+ }
1116
+ else {
1117
+ throw 'Must specify a callout id.';
1118
+ }
1119
+ return callout;
1120
+ };
1121
+
1122
+ /**
1123
+ * getCallout
1124
+ *
1125
+ * Returns a callout by its id.
1126
+ *
1127
+ * @param {String} id The id of the callout to fetch.
1128
+ * @returns {Object} HopscotchBubble
1129
+ */
1130
+ this.getCallout = function(id) {
1131
+ return callouts[id];
1132
+ };
1133
+
1134
+ /**
1135
+ * removeAllCallouts
1136
+ *
1137
+ * Removes all existing callouts.
1138
+ */
1139
+ this.removeAllCallouts = function() {
1140
+ var calloutId,
1141
+ callout;
1142
+
1143
+ for (calloutId in callouts) {
1144
+ if (callouts.hasOwnProperty(calloutId)) {
1145
+ this.removeCallout(calloutId);
1146
+ }
1147
+ }
1148
+ };
1149
+
1150
+ /**
1151
+ * removeAllCallout
1152
+ *
1153
+ * Removes an existing callout by id.
1154
+ *
1155
+ * @param {String} id The id of the callout to remove.
1156
+ */
1157
+ this.removeCallout = function(id) {
1158
+ var callout = callouts[id];
1159
+
1160
+ callouts[id] = null;
1161
+ if (!callout) { return; }
1162
+
1163
+ callout.destroy();
1164
+ };
1165
+ };
1166
+
1167
+ /**
1168
+ * Hopscotch
1169
+ *
1170
+ * @class Creates the Hopscotch object. Used to manage tour progress and configurations.
1171
+ * @constructor
1172
+ * @param {Object} initOptions Options to be passed to `configure()`.
1173
+ */
1174
+ Hopscotch = function(initOptions) {
1175
+ var self = this, // for targetClickNextFn
1176
+ bubble,
1177
+ calloutMgr,
1178
+ opt,
1179
+ currTour,
1180
+ currStepNum,
1181
+ cookieTourId,
1182
+ cookieTourStep,
1183
+ _configure,
1184
+
1185
+ /**
1186
+ * getBubble
1187
+ *
1188
+ * Singleton accessor function for retrieving or creating bubble object.
1189
+ *
1190
+ * @private
1191
+ * @param setOptions {Boolean} when true, transfers configuration options to the bubble
1192
+ * @returns {Object} HopscotchBubble
1193
+ */
1194
+ getBubble = function(setOptions) {
1195
+ if (!bubble) {
1196
+ bubble = new HopscotchBubble(opt);
1197
+ }
1198
+ if (setOptions) {
1199
+ utils.extend(bubble.opt, {
1200
+ bubblePadding: getOption('bubblePadding'),
1201
+ bubbleWidth: getOption('bubbleWidth'),
1202
+ showNextButton: getOption('showNextButton'),
1203
+ showPrevButton: getOption('showPrevButton'),
1204
+ showCloseButton: getOption('showCloseButton'),
1205
+ arrowWidth: getOption('arrowWidth')
1206
+ });
1207
+ }
1208
+ return bubble;
1209
+ },
1210
+
1211
+ /**
1212
+ * Convenience method for getting an option. Returns custom config option
1213
+ * or the default config option if no custom value exists.
1214
+ *
1215
+ * @private
1216
+ * @param name {String} config option name
1217
+ * @returns {Object} config option value
1218
+ */
1219
+ getOption = function(name) {
1220
+ if (typeof opt === 'undefined') {
1221
+ return defaultOpts[name];
1222
+ }
1223
+ return utils.valOrDefault(opt[name], defaultOpts[name]);
1224
+ },
1225
+
1226
+ /**
1227
+ * getCurrStep
1228
+ *
1229
+ * @private
1230
+ * @returns {Object} the step object corresponding to the current value of currStepNum
1231
+ */
1232
+ getCurrStep = function() {
1233
+ var step;
1234
+
1235
+ if (currStepNum < 0 || currStepNum >= currTour.steps.length) {
1236
+ step = null;
1237
+ }
1238
+ else {
1239
+ step = currTour.steps[currStepNum];
1240
+ }
1241
+
1242
+ return step;
1243
+ },
1244
+
1245
+ /**
1246
+ * Used for nextOnTargetClick
1247
+ *
1248
+ * @private
1249
+ */
1250
+ targetClickNextFn = function() {
1251
+ self.nextStep();
1252
+ },
1253
+
1254
+ /**
1255
+ * adjustWindowScroll
1256
+ *
1257
+ * Checks if the bubble or target element is partially or completely
1258
+ * outside of the viewport. If it is, adjust the window scroll position
1259
+ * to bring it back into the viewport.
1260
+ *
1261
+ * @private
1262
+ * @param {Function} cb Callback to invoke after done scrolling.
1263
+ */
1264
+ adjustWindowScroll = function(cb) {
1265
+ var bubble = getBubble(),
1266
+
1267
+ // Calculate the bubble element top and bottom position
1268
+ bubbleEl = bubble.element,
1269
+ bubbleTop = utils.getPixelValue(bubbleEl.style.top),
1270
+ bubbleBottom = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight),
1271
+
1272
+ // Calculate the target element top and bottom position
1273
+ targetEl = utils.getStepTarget(getCurrStep()),
1274
+ targetBounds = targetEl.getBoundingClientRect(),
1275
+ targetElTop = targetBounds.top + utils.getScrollTop(),
1276
+ targetElBottom = targetBounds.bottom + utils.getScrollTop(),
1277
+
1278
+ // The higher of the two: bubble or target
1279
+ targetTop = (bubbleTop < targetElTop) ? bubbleTop : targetElTop,
1280
+ // The lower of the two: bubble or target
1281
+ targetBottom = (bubbleBottom > targetElBottom) ? bubbleBottom : targetElBottom,
1282
+
1283
+ // Calculate the current viewport top and bottom
1284
+ windowTop = utils.getScrollTop(),
1285
+ windowBottom = windowTop + utils.getWindowHeight(),
1286
+
1287
+ // This is our final target scroll value.
1288
+ scrollToVal = targetTop - getOption('scrollTopMargin'),
1289
+
1290
+ scrollEl,
1291
+ yuiAnim,
1292
+ yuiEase,
1293
+ direction,
1294
+ scrollIncr,
1295
+ scrollTimeout,
1296
+ scrollTimeoutFn;
1297
+
1298
+ // Target and bubble are both visible in viewport
1299
+ if (targetTop >= windowTop && (targetTop <= windowTop + getOption('scrollTopMargin') || targetBottom <= windowBottom)) {
1300
+ if (cb) { cb(); } // HopscotchBubble.show
1301
+ }
1302
+
1303
+ // Abrupt scroll to scroll target
1304
+ else if (!getOption('smoothScroll')) {
1305
+ window.scrollTo(0, scrollToVal);
1306
+
1307
+ if (cb) { cb(); } // HopscotchBubble.show
1308
+ }
1309
+
1310
+ // Smooth scroll to scroll target
1311
+ else {
1312
+ // Use YUI if it exists
1313
+ if (typeof YAHOO !== undefinedStr &&
1314
+ typeof YAHOO.env !== undefinedStr &&
1315
+ typeof YAHOO.env.ua !== undefinedStr &&
1316
+ typeof YAHOO.util !== undefinedStr &&
1317
+ typeof YAHOO.util.Scroll !== undefinedStr) {
1318
+ scrollEl = YAHOO.env.ua.webkit ? document.body : document.documentElement;
1319
+ yuiEase = YAHOO.util.Easing ? YAHOO.util.Easing.easeOut : undefined;
1320
+ yuiAnim = new YAHOO.util.Scroll(scrollEl, {
1321
+ scroll: { to: [0, scrollToVal] }
1322
+ }, getOption('scrollDuration')/1000, yuiEase);
1323
+ yuiAnim.onComplete.subscribe(cb);
1324
+ yuiAnim.animate();
1325
+ }
1326
+
1327
+ // Use jQuery if it exists
1328
+ else if (hasJquery) {
1329
+ jQuery('body, html').animate({ scrollTop: scrollToVal }, getOption('scrollDuration'), cb);
1330
+ }
1331
+
1332
+ // Use my crummy setInterval scroll solution if we're using plain, vanilla Javascript.
1333
+ else {
1334
+ if (scrollToVal < 0) {
1335
+ scrollToVal = 0;
1336
+ }
1337
+
1338
+ // 48 * 10 == 480ms scroll duration
1339
+ // make it slightly less than CSS transition duration because of
1340
+ // setInterval overhead.
1341
+ // To increase or decrease duration, change the divisor of scrollIncr.
1342
+ direction = (windowTop > targetTop) ? -1 : 1; // -1 means scrolling up, 1 means down
1343
+ scrollIncr = Math.abs(windowTop - scrollToVal) / (getOption('scrollDuration')/10);
1344
+ scrollTimeoutFn = function() {
1345
+ var scrollTop = utils.getScrollTop(),
1346
+ scrollTarget = scrollTop + (direction * scrollIncr);
1347
+
1348
+ if ((direction > 0 && scrollTarget >= scrollToVal) ||
1349
+ (direction < 0 && scrollTarget <= scrollToVal)) {
1350
+ // Overshot our target. Just manually set to equal the target
1351
+ // and clear the interval
1352
+ scrollTarget = scrollToVal;
1353
+ if (cb) { cb(); } // HopscotchBubble.show
1354
+ window.scrollTo(0, scrollTarget);
1355
+ return;
1356
+ }
1357
+
1358
+ window.scrollTo(0, scrollTarget);
1359
+
1360
+ if (utils.getScrollTop() === scrollTop) {
1361
+ // Couldn't scroll any further.
1362
+ if (cb) { cb(); } // HopscotchBubble.show
1363
+ return;
1364
+ }
1365
+
1366
+ // If we reached this point, that means there's still more to scroll.
1367
+ setTimeout(scrollTimeoutFn, 10);
1368
+ };
1369
+
1370
+ scrollTimeoutFn();
1371
+ }
1372
+ }
1373
+ },
1374
+
1375
+ /**
1376
+ * goToStepWithTarget
1377
+ *
1378
+ * Helper function to increment the step number until a step is found where
1379
+ * the step target exists or until we reach the end/beginning of the tour.
1380
+ *
1381
+ * @private
1382
+ * @param {Number} direction Either 1 for incrementing or -1 for decrementing
1383
+ * @param {Function} cb The callback function to be invoked when the step has been found
1384
+ */
1385
+ goToStepWithTarget = function(direction, cb) {
1386
+ var target,
1387
+ step,
1388
+ goToStepFn;
1389
+
1390
+ if (currStepNum + direction >= 0 &&
1391
+ currStepNum + direction < currTour.steps.length) {
1392
+
1393
+ currStepNum += direction;
1394
+ step = getCurrStep();
1395
+
1396
+ goToStepFn = function() {
1397
+ target = utils.getStepTarget(step);
1398
+
1399
+ if (target) {
1400
+ // We're done! Return the step number via the callback.
1401
+ cb(currStepNum);
1402
+ }
1403
+ else {
1404
+ // Haven't found a valid target yet. Recursively call
1405
+ // goToStepWithTarget.
1406
+ utils.invokeEventCallbacks('error');
1407
+ goToStepWithTarget(direction, cb);
1408
+ }
1409
+ };
1410
+
1411
+ if (step.delay) {
1412
+ setTimeout(goToStepFn, step.delay);
1413
+ }
1414
+ else {
1415
+ goToStepFn();
1416
+ }
1417
+ }
1418
+ else {
1419
+ cb(-1); // signal that we didn't find any step with a valid target
1420
+ }
1421
+ },
1422
+
1423
+ /**
1424
+ * changeStep
1425
+ *
1426
+ * Helper function to change step by going forwards or backwards 1.
1427
+ * nextStep and prevStep are publicly accessible wrappers for this function.
1428
+ *
1429
+ * @private
1430
+ * @param {Boolean} doCallbacks Flag for invoking onNext or onPrev callbacks
1431
+ * @param {Number} direction Either 1 for "next" or -1 for "prev"
1432
+ */
1433
+ changeStep = function(doCallbacks, direction) {
1434
+ var bubble = getBubble(),
1435
+ self = this,
1436
+ step,
1437
+ origStep,
1438
+ wasMultiPage,
1439
+ changeStepCb;
1440
+
1441
+ bubble.hide();
1442
+
1443
+ doCallbacks = utils.valOrDefault(doCallbacks, true);
1444
+ step = getCurrStep();
1445
+ origStep = step;
1446
+ if (direction > 0) {
1447
+ wasMultiPage = origStep.multipage;
1448
+ }
1449
+ else {
1450
+ wasMultiPage = (currStepNum > 0 && currTour.steps[currStepNum-1].multipage);
1451
+ }
1452
+
1453
+ /**
1454
+ * Callback for goToStepWithTarget
1455
+ *
1456
+ * @private
1457
+ */
1458
+ changeStepCb = function(stepNum) {
1459
+ var doShowFollowingStep;
1460
+
1461
+ if (stepNum === -1) {
1462
+ // Wasn't able to find a step with an existing element. End tour.
1463
+ return this.endTour(true);
1464
+ }
1465
+
1466
+ if (doCallbacks) {
1467
+ if (direction > 0) {
1468
+ doShowFollowingStep = utils.invokeEventCallbacks('next', origStep.onNext);
1469
+ }
1470
+ else {
1471
+ doShowFollowingStep = utils.invokeEventCallbacks('prev', origStep.onPrev);
1472
+ }
1473
+ }
1474
+
1475
+ // If the state of the tour is updated in a callback, assume the client
1476
+ // doesn't want to go to next step since they specifically updated.
1477
+ if (stepNum !== currStepNum) {
1478
+ return;
1479
+ }
1480
+
1481
+ if (wasMultiPage) {
1482
+ // Update state for the next page
1483
+ utils.setState(getOption('cookieName'), currTour.id + ':' + currStepNum, 1);
1484
+
1485
+ // Next step is on a different page, so no need to attempt to render it.
1486
+ return;
1487
+ }
1488
+
1489
+ doShowFollowingStep = utils.valOrDefault(doShowFollowingStep, true);
1490
+
1491
+ // If the onNext/onPrev callback returned false, halt the tour and
1492
+ // don't show the next step.
1493
+ if (doShowFollowingStep) {
1494
+ this.showStep(stepNum);
1495
+ }
1496
+ else {
1497
+ // Halt tour (but don't clear state)
1498
+ this.endTour(false);
1499
+ }
1500
+ };
1501
+
1502
+ if (!wasMultiPage && getOption('skipIfNoElement')) {
1503
+ goToStepWithTarget(direction, function(stepNum) {
1504
+ changeStepCb.call(self, stepNum);
1505
+ });
1506
+ }
1507
+ else if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) {
1508
+ // only try incrementing once, and invoke error callback if no target is found
1509
+ currStepNum += direction;
1510
+ step = getCurrStep();
1511
+ if (!utils.getStepTarget(step) && !wasMultiPage) {
1512
+ utils.invokeEventCallbacks('error');
1513
+ return this.endTour(true, false);
1514
+ }
1515
+ changeStepCb.call(this, currStepNum);
1516
+ }
1517
+
1518
+ return this;
1519
+ },
1520
+
1521
+ /**
1522
+ * loadTour
1523
+ *
1524
+ * Loads, but does not display, tour.
1525
+ *
1526
+ * @private
1527
+ * @param tour The tour JSON object
1528
+ */
1529
+ loadTour = function(tour) {
1530
+ var tmpOpt = {},
1531
+ prop,
1532
+ tourState,
1533
+ tourPair;
1534
+
1535
+ // Set tour-specific configurations
1536
+ for (prop in tour) {
1537
+ if (tour.hasOwnProperty(prop) &&
1538
+ prop !== 'id' &&
1539
+ prop !== 'steps') {
1540
+ tmpOpt[prop] = tour[prop];
1541
+ }
1542
+ }
1543
+
1544
+ //this.resetDefaultOptions(); // reset all options so there are no surprises
1545
+ // TODO check number of config properties of tour
1546
+ _configure.call(this, tmpOpt, true);
1547
+
1548
+ // Get existing tour state, if it exists.
1549
+ tourState = utils.getState(getOption('cookieName'));
1550
+ if (tourState) {
1551
+ tourPair = tourState.split(':');
1552
+ cookieTourId = tourPair[0]; // selecting tour is not supported by this framework.
1553
+ cookieTourStep = tourPair[1];
1554
+
1555
+ cookieTourStep = parseInt(cookieTourStep, 10);
1556
+ }
1557
+
1558
+ return this;
1559
+ },
1560
+
1561
+ /**
1562
+ * Find the first step to show for a tour. (What is the first step with a
1563
+ * target on the page?)
1564
+ */
1565
+ findStartingStep = function(startStepNum, cb) {
1566
+ var step,
1567
+ target,
1568
+ stepNum;
1569
+
1570
+ currStepNum = startStepNum || 0;
1571
+ step = getCurrStep();
1572
+ target = utils.getStepTarget(step);
1573
+
1574
+ if (target) {
1575
+ // First step had an existing target.
1576
+ cb(currStepNum);
1577
+ return;
1578
+ }
1579
+
1580
+ if (!target) {
1581
+ // Previous target doesn't exist either. The user may have just
1582
+ // clicked on a link that wasn't part of the tour. Another possibility is that
1583
+ // the user clicked on the correct link, but the target is just missing for
1584
+ // whatever reason. In either case, we should just advance until we find a step
1585
+ // that has a target on the page or end the tour if we can't find such a step.
1586
+ utils.invokeEventCallbacks('error');
1587
+
1588
+ if (getOption('skipIfNoElement')) {
1589
+ goToStepWithTarget(1, cb);
1590
+ return;
1591
+ }
1592
+ else {
1593
+ currStepNum = -1;
1594
+ cb(currStepNum);
1595
+ }
1596
+ }
1597
+ },
1598
+
1599
+ showStepHelper = function(stepNum) {
1600
+ var step = currTour.steps[stepNum],
1601
+ tourSteps = currTour.steps,
1602
+ numTourSteps = tourSteps.length,
1603
+ cookieVal = currTour.id + ':' + stepNum,
1604
+ bubble = getBubble(),
1605
+ targetEl = utils.getStepTarget(step),
1606
+ isLast,
1607
+ showBubble;
1608
+
1609
+ showBubble = function() {
1610
+ bubble.show();
1611
+ utils.invokeEventCallbacks('show', step.onShow);
1612
+ };
1613
+
1614
+ // Update bubble for current step
1615
+ currStepNum = stepNum;
1616
+
1617
+ bubble.hide(false);
1618
+
1619
+ isLast = (stepNum === numTourSteps - 1);
1620
+ bubble.render(step, stepNum, function(adjustScroll) {
1621
+ // when done adjusting window scroll, call showBubble helper fn
1622
+ if (adjustScroll) {
1623
+ adjustWindowScroll(showBubble);
1624
+ }
1625
+ else {
1626
+ showBubble();
1627
+ }
1628
+
1629
+ // If we want to advance to next step when user clicks on target.
1630
+ if (step.nextOnTargetClick) {
1631
+ utils.addEvtListener(targetEl, 'click', targetClickNextFn);
1632
+ }
1633
+ });
1634
+
1635
+ utils.setState(getOption('cookieName'), cookieVal, 1);
1636
+ },
1637
+
1638
+ /**
1639
+ * init
1640
+ *
1641
+ * Initializes the Hopscotch object.
1642
+ *
1643
+ * @private
1644
+ */
1645
+ init = function(initOptions) {
1646
+ if (initOptions) {
1647
+ //initOptions.cookieName = initOptions.cookieName || 'hopscotch.tour.state';
1648
+ this.configure(initOptions);
1649
+ }
1650
+ };
1651
+
1652
+ /**
1653
+ * getCalloutManager
1654
+ *
1655
+ * Gets the callout manager.
1656
+ *
1657
+ * @returns {Object} HopscotchCalloutManager
1658
+ *
1659
+ */
1660
+ this.getCalloutManager = function() {
1661
+ if (typeof calloutMgr === undefinedStr) {
1662
+ calloutMgr = new HopscotchCalloutManager();
1663
+ }
1664
+
1665
+ return calloutMgr;
1666
+ };
1667
+
1668
+ /**
1669
+ * startTour
1670
+ *
1671
+ * Begins the tour.
1672
+ *
1673
+ * @param {Object} tour The tour JSON object
1674
+ * @stepNum {Number} stepNum __Optional__ The step number to start from
1675
+ * @returns {Object} Hopscotch
1676
+ *
1677
+ */
1678
+ this.startTour = function(tour, stepNum) {
1679
+ var bubble,
1680
+ currStepNum,
1681
+ self = this;
1682
+
1683
+ // loadTour if we are calling startTour directly. (When we call startTour
1684
+ // from window onLoad handler, we'll use currTour)
1685
+ if (!currTour) {
1686
+ currTour = tour;
1687
+ loadTour.call(this, tour);
1688
+ }
1689
+
1690
+ if (typeof stepNum !== undefinedStr) {
1691
+ if (stepNum >= currTour.steps.length) {
1692
+ throw 'Specified step number out of bounds.';
1693
+ }
1694
+ currStepNum = stepNum;
1695
+ }
1696
+
1697
+ // If document isn't ready, wait for it to finish loading.
1698
+ // (so that we can calculate positioning accurately)
1699
+ if (!utils.documentIsReady()) {
1700
+ waitingToStart = true;
1701
+ return this;
1702
+ }
1703
+
1704
+ if (typeof currStepNum === "undefined" && currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) {
1705
+ currStepNum = cookieTourStep;
1706
+ }
1707
+ else if (!currStepNum) {
1708
+ currStepNum = 0;
1709
+ }
1710
+
1711
+ // Find the current step we should begin the tour on, and then actually start the tour.
1712
+ findStartingStep(currStepNum, function(stepNum) {
1713
+ var target = (stepNum !== -1) && utils.getStepTarget(currTour.steps[stepNum]);
1714
+
1715
+ if (!target) {
1716
+ // Should we trigger onEnd callback? Let's err on the side of caution
1717
+ // and not trigger it. Don't want weird stuff happening on a page that
1718
+ // wasn't meant for the tour. Up to the developer to fix their tour.
1719
+ self.endTour(false, false);
1720
+ return;
1721
+ }
1722
+
1723
+ utils.invokeEventCallbacks('start');
1724
+
1725
+ bubble = getBubble();
1726
+ // TODO: do we still need this call to .hide()? No longer using opt.animate...
1727
+ // Leaving it in for now to play it safe
1728
+ bubble.hide(false); // make invisible for boundingRect calculations when opt.animate == true
1729
+
1730
+ self.isActive = true;
1731
+
1732
+ if (!utils.getStepTarget(getCurrStep())) {
1733
+ // First step element doesn't exist
1734
+ utils.invokeEventCallbacks('error');
1735
+ if (getOption('skipIfNoElement')) {
1736
+ self.nextStep(false);
1737
+ }
1738
+ }
1739
+ else {
1740
+ self.showStep(stepNum);
1741
+ }
1742
+ });
1743
+
1744
+ return this;
1745
+ };
1746
+
1747
+ /**
1748
+ * showStep
1749
+ *
1750
+ * Skips to a specific step and renders the corresponding bubble.
1751
+ *
1752
+ * @stepNum {Number} stepNum The step number to show
1753
+ * @returns {Object} Hopscotch
1754
+ */
1755
+ this.showStep = function(stepNum) {
1756
+ var step = currTour.steps[stepNum];
1757
+ if (step.delay) {
1758
+ setTimeout(function() {
1759
+ showStepHelper(stepNum);
1760
+ }, step.delay);
1761
+ }
1762
+ else {
1763
+ showStepHelper(stepNum);
1764
+ }
1765
+ return this;
1766
+ };
1767
+
1768
+ /**
1769
+ * prevStep
1770
+ *
1771
+ * Jump to the previous step.
1772
+ *
1773
+ * @param {Boolean} doCallbacks Flag for invoking onPrev callback. Defaults to true.
1774
+ * @returns {Object} Hopscotch
1775
+ */
1776
+ this.prevStep = function(doCallbacks) {
1777
+ changeStep.call(this, doCallbacks, -1);
1778
+ return this;
1779
+ };
1780
+
1781
+ /**
1782
+ * nextStep
1783
+ *
1784
+ * Jump to the next step.
1785
+ *
1786
+ * @param {Boolean} doCallbacks Flag for invoking onNext callback. Defaults to true.
1787
+ * @returns {Object} Hopscotch
1788
+ */
1789
+ this.nextStep = function(doCallbacks) {
1790
+ var step = getCurrStep(),
1791
+ targetEl = utils.getStepTarget(step);
1792
+
1793
+ if (step.nextOnTargetClick) {
1794
+ // Detach the listener after we've clicked on the target OR the next button.
1795
+ utils.removeEvtListener(targetEl, 'click', targetClickNextFn);
1796
+ }
1797
+ changeStep.call(this, doCallbacks, 1);
1798
+ return this;
1799
+ };
1800
+
1801
+ /**
1802
+ * endTour
1803
+ *
1804
+ * Cancels out of an active tour.
1805
+ *
1806
+ * @param {Boolean} clearState Flag for clearing state. Defaults to true.
1807
+ * @param {Boolean} doCallbacks Flag for invoking 'onEnd' callbacks. Defaults to true.
1808
+ * @returns {Object} Hopscotch
1809
+ */
1810
+ this.endTour = function(clearState, doCallbacks) {
1811
+ var bubble = getBubble();
1812
+ clearState = utils.valOrDefault(clearState, true);
1813
+ doCallbacks = utils.valOrDefault(doCallbacks, true);
1814
+ currStepNum = 0;
1815
+ cookieTourStep = undefined;
1816
+
1817
+ bubble.hide();
1818
+ if (clearState) {
1819
+ utils.clearState(getOption('cookieName'));
1820
+ }
1821
+ if (this.isActive) {
1822
+ this.isActive = false;
1823
+
1824
+ if (currTour && doCallbacks) {
1825
+ utils.invokeEventCallbacks('end');
1826
+ }
1827
+ }
1828
+
1829
+ this.removeCallbacks(null, true);
1830
+ this.resetDefaultOptions();
1831
+
1832
+ currTour = null;
1833
+
1834
+ return this;
1835
+ };
1836
+
1837
+ /**
1838
+ * getCurrTour
1839
+ *
1840
+ * @return {Object} The currently loaded tour.
1841
+ */
1842
+ this.getCurrTour = function() {
1843
+ return currTour;
1844
+ };
1845
+
1846
+ /**
1847
+ * getCurrTarget
1848
+ *
1849
+ * @return {Object} The currently visible target.
1850
+ */
1851
+ this.getCurrTarget = function() {
1852
+ return utils.getStepTarget(getCurrStep());
1853
+ };
1854
+
1855
+ /**
1856
+ * getCurrStepNum
1857
+ *
1858
+ * @return {number} The current zero-based step number.
1859
+ */
1860
+ this.getCurrStepNum = function() {
1861
+ return currStepNum;
1862
+ };
1863
+
1864
+ /**
1865
+ * refreshBubblePosition
1866
+ *
1867
+ * Tell hopscotch that the position of the current tour element changed
1868
+ * and the bubble therefore needs to be redrawn
1869
+ *
1870
+ * @returns {Object} Hopscotch
1871
+ */
1872
+ this.refreshBubblePosition = function() {
1873
+ bubble.setPosition(getCurrStep());
1874
+ return this;
1875
+ };
1876
+
1877
+ /**
1878
+ * listen
1879
+ *
1880
+ * Adds a callback for one of the event types. Valid event types are:
1881
+ *
1882
+ * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
1883
+ * @param {Function} cb The callback to add.
1884
+ * @param {Boolean} isTourCb Flag indicating callback is from a tour definition.
1885
+ * For internal use only!
1886
+ * @returns {Object} Hopscotch
1887
+ */
1888
+ this.listen = function(evtType, cb, isTourCb) {
1889
+ if (evtType) {
1890
+ callbacks[evtType].push({ cb: cb, fromTour: isTourCb });
1891
+ }
1892
+ return this;
1893
+ };
1894
+
1895
+ /**
1896
+ * unlisten
1897
+ *
1898
+ * Removes a callback for one of the event types, e.g. 'start', 'next', etc.
1899
+ *
1900
+ * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
1901
+ * @param {Function} cb The callback to remove.
1902
+ * @returns {Object} Hopscotch
1903
+ */
1904
+ this.unlisten = function(evtType, cb) {
1905
+ var evtCallbacks = callbacks[evtType],
1906
+ i,
1907
+ len;
1908
+
1909
+ for (i = 0, len = evtCallbacks.length; i < len; ++i) {
1910
+ if (evtCallbacks[i] === cb) {
1911
+ evtCallbacks.splice(i, 1);
1912
+ }
1913
+ }
1914
+ return this;
1915
+ };
1916
+
1917
+ /**
1918
+ * removeCallbacks
1919
+ *
1920
+ * Remove callbacks for hopscotch events. If tourOnly is set to true, only
1921
+ * removes callbacks specified by a tour (callbacks set by external calls
1922
+ * to hopscotch.configure or hopscotch.listen will not be removed). If
1923
+ * evtName is null or undefined, callbacks for all events will be removed.
1924
+ *
1925
+ * @param {string} evtName Optional Event name for which we should remove callbacks
1926
+ * @param {boolean} tourOnly Optional flag to indicate we should only remove callbacks added
1927
+ * by a tour. Defaults to false.
1928
+ * @returns {Object} Hopscotch
1929
+ */
1930
+ this.removeCallbacks = function(evtName, tourOnly) {
1931
+ var cbArr,
1932
+ i,
1933
+ len,
1934
+ evt;
1935
+
1936
+ // If evtName is null or undefined, remove callbacks for all events.
1937
+ for (evt in callbacks) {
1938
+ if (!evtName || evtName === evt) {
1939
+ if (tourOnly) {
1940
+ cbArr = callbacks[evt];
1941
+ for (i=0, len=cbArr.length; i < len; ++i) {
1942
+ if (cbArr[i].fromTour) {
1943
+ cbArr.splice(i--, 1);
1944
+ --len;
1945
+ }
1946
+ }
1947
+ }
1948
+ else {
1949
+ callbacks[evt] = [];
1950
+ }
1951
+ }
1952
+ }
1953
+ return this;
1954
+ };
1955
+
1956
+ /**
1957
+ * registerHelper
1958
+ * ==============
1959
+ * Registers a helper function to be used as a callback function.
1960
+ *
1961
+ * @param {String} id The id of the function.
1962
+ * @param {Function} id The callback function.
1963
+ */
1964
+ this.registerHelper = function(id, fn) {
1965
+ if (typeof id === 'string' && typeof fn === 'function') {
1966
+ helpers[id] = fn;
1967
+ }
1968
+ };
1969
+
1970
+ this.unregisterHelper = function(id) {
1971
+ helpers[id] = null;
1972
+ };
1973
+
1974
+ this.invokeHelper = function(id) {
1975
+ var args = [],
1976
+ i,
1977
+ len;
1978
+
1979
+ for (i = 1, len = arguments.length; i < len; ++i) {
1980
+ args.push(arguments[i]);
1981
+ }
1982
+ if (helpers[id]) {
1983
+ helpers[id].call(null, args);
1984
+ }
1985
+ };
1986
+
1987
+ /**
1988
+ * setCookieName
1989
+ *
1990
+ * Sets the cookie name (or sessionStorage name, if supported) used for multi-page
1991
+ * tour persistence.
1992
+ *
1993
+ * @param {String} name The cookie name
1994
+ * @returns {Object} Hopscotch
1995
+ */
1996
+ this.setCookieName = function(name) {
1997
+ opt.cookieName = name;
1998
+ return this;
1999
+ };
2000
+
2001
+ /**
2002
+ * resetDefaultOptions
2003
+ *
2004
+ * Resets all configuration options to default.
2005
+ *
2006
+ * @returns {Object} Hopscotch
2007
+ */
2008
+ this.resetDefaultOptions = function() {
2009
+ opt = {};
2010
+ return this;
2011
+ };
2012
+
2013
+ /**
2014
+ * resetDefaultI18N
2015
+ *
2016
+ * Resets all i18n.
2017
+ *
2018
+ * @returns {Object} Hopscotch
2019
+ */
2020
+ this.resetDefaultI18N = function() {
2021
+ customI18N = {};
2022
+ return this;
2023
+ };
2024
+
2025
+ /**
2026
+ * hasState
2027
+ *
2028
+ * Returns state from a previous tour run, if it exists.
2029
+ *
2030
+ * @returns {String} State of previous tour run, or empty string if none exists.
2031
+ */
2032
+ this.getState = function() {
2033
+ return utils.getState(getOption('cookieName'));
2034
+ };
2035
+
2036
+ /**
2037
+ * _configure
2038
+ *
2039
+ * @see this.configure
2040
+ * @private
2041
+ * @param options
2042
+ * @param {Boolean} isTourOptions Should be set to true when setting options from a tour definition.
2043
+ */
2044
+ _configure = function(options, isTourOptions) {
2045
+ var bubble,
2046
+ events = ['next', 'prev', 'start', 'end', 'show', 'error', 'close'],
2047
+ eventPropName,
2048
+ callbackProp,
2049
+ i,
2050
+ len;
2051
+
2052
+ if (!opt) {
2053
+ this.resetDefaultOptions();
2054
+ }
2055
+
2056
+ utils.extend(opt, options);
2057
+
2058
+ if (options) {
2059
+ utils.extend(customI18N, options.i18n);
2060
+ }
2061
+
2062
+ for (i = 0, len = events.length; i < len; ++i) {
2063
+ // At this point, options[eventPropName] may have changed from an array
2064
+ // to a function.
2065
+ eventPropName = 'on' + events[i].charAt(0).toUpperCase() + events[i].substring(1);
2066
+ if (options[eventPropName]) {
2067
+ this.listen(events[i],
2068
+ options[eventPropName],
2069
+ isTourOptions);
2070
+ }
2071
+ }
2072
+
2073
+ bubble = getBubble(true);
2074
+
2075
+ return this;
2076
+ };
2077
+
2078
+ /**
2079
+ * configure
2080
+ *
2081
+ * <pre>
2082
+ * VALID OPTIONS INCLUDE...
2083
+ *
2084
+ * - bubbleWidth: Number - Default bubble width. Defaults to 280.
2085
+ * - bubblePadding: Number - DEPRECATED. Default bubble padding. Defaults to 15.
2086
+ * - smoothScroll: Boolean - should the page scroll smoothly to the next
2087
+ * step? Defaults to TRUE.
2088
+ * - scrollDuration: Number - Duration of page scroll. Only relevant when
2089
+ * smoothScroll is set to true. Defaults to
2090
+ * 1000ms.
2091
+ * - scrollTopMargin: NUMBER - When the page scrolls, how much space should there
2092
+ * be between the bubble/targetElement and the top
2093
+ * of the viewport? Defaults to 200.
2094
+ * - showCloseButton: Boolean - should the tour bubble show a close (X) button?
2095
+ * Defaults to TRUE.
2096
+ * - showPrevButton: Boolean - should the bubble have the Previous button?
2097
+ * Defaults to FALSE.
2098
+ * - showNextButton: Boolean - should the bubble have the Next button?
2099
+ * Defaults to TRUE.
2100
+ * - arrowWidth: Number - Default arrow width. (space between the bubble
2101
+ * and the targetEl) Used for bubble position
2102
+ * calculation. Only use this option if you are
2103
+ * using your own custom CSS. Defaults to 20.
2104
+ * - skipIfNoElement Boolean - If a specified target element is not found,
2105
+ * should we skip to the next step? Defaults to
2106
+ * TRUE.
2107
+ * - onNext: Function - A callback to be invoked after every click on
2108
+ * a "Next" button.
2109
+ *
2110
+ * - i18n: Object - For i18n purposes. Allows you to change the
2111
+ * text of button labels and step numbers.
2112
+ * - i18n.stepNums: Array\<String\> - Provide a list of strings to be shown as
2113
+ * the step number, based on index of array. Unicode
2114
+ * characters are supported. (e.g., ['&#x4e00;',
2115
+ * '&#x4e8c;', '&#x4e09;']) If there are more steps
2116
+ * than provided numbers, Arabic numerals
2117
+ * ('4', '5', '6', etc.) will be used as default.
2118
+ * - highlight: Boolean - Shows an overlay that highlights the selected element
2119
+ * Defaults to FALSE.
2120
+ * - highlightMargin: Number - Amount of margin around the selected element to show
2121
+ * Defaults to 0
2122
+ *
2123
+ * // =========
2124
+ * // CALLBACKS
2125
+ * // =========
2126
+ * - onNext: Function - Invoked after every click on a "Next" button.
2127
+ * - onPrev: Function - Invoked after every click on a "Prev" button.
2128
+ * - onStart: Function - Invoked when the tour is started.
2129
+ * - onEnd: Function - Invoked when the tour ends.
2130
+ * - onClose: Function - Invoked when the user closes the tour before finishing.
2131
+ * - onError: Function - Invoked when the specified target element doesn't exist on the page.
2132
+ *
2133
+ * // ====
2134
+ * // I18N
2135
+ * // ====
2136
+ * i18n: OBJECT - For i18n purposes. Allows you to change the text
2137
+ * of button labels and step numbers.
2138
+ * i18n.nextBtn: STRING - Label for next button
2139
+ * i18n.prevBtn: STRING - Label for prev button
2140
+ * i18n.doneBtn: STRING - Label for done button
2141
+ * i18n.skipBtn: STRING - Label for skip button
2142
+ * i18n.closeTooltip: STRING - Text for close button tooltip
2143
+ * i18n.stepNums: ARRAY<STRING> - Provide a list of strings to be shown as
2144
+ * the step number, based on index of array. Unicode
2145
+ * characters are supported. (e.g., ['&#x4e00;',
2146
+ * '&#x4e8c;', '&#x4e09;']) If there are more steps
2147
+ * than provided numbers, Arabic numerals
2148
+ * ('4', '5', '6', etc.) will be used as default.
2149
+ * </pre>
2150
+ *
2151
+ * @example hopscotch.configure({ scrollDuration: 1000, scrollTopMargin: 150 });
2152
+ * @example
2153
+ * hopscotch.configure({
2154
+ * scrollTopMargin: 150,
2155
+ * onStart: function() {
2156
+ * alert("Have fun!");
2157
+ * },
2158
+ * i18n: {
2159
+ * nextBtn: 'Forward',
2160
+ * prevBtn: 'Previous'
2161
+ * closeTooltip: 'Quit'
2162
+ * }
2163
+ * });
2164
+ *
2165
+ * @param {Object} options A hash of configuration options.
2166
+ * @returns {Object} Hopscotch
2167
+ */
2168
+ this.configure = function(options) {
2169
+ return _configure.call(this, options, false);
2170
+ };
2171
+
2172
+ /**
2173
+ * Set the template that should be used for rendering Hopscotch bubbles.
2174
+ * If a string, it's assumed your template is available in the
2175
+ * hopscotch.templates namespace.
2176
+ *
2177
+ * @param {String|Function(obj)} The template to use for rendering.
2178
+ * @returns {Object} The Hopscotch object (for chaining).
2179
+ */
2180
+ this.setRenderer = function(render){
2181
+ var typeOfRender = typeof render;
2182
+
2183
+ if(typeOfRender === 'string'){
2184
+ templateToUse = render;
2185
+ customRenderer = undefined;
2186
+ }
2187
+ else if(typeOfRender === 'function'){
2188
+ customRenderer = render;
2189
+ }
2190
+ return this;
2191
+ };
2192
+
2193
+ /**
2194
+ * Sets the escaping method to be used by JST templates.
2195
+ *
2196
+ * @param {Function} - The escape method to use.
2197
+ * @returns {Object} The Hopscotch object (for chaining).
2198
+ */
2199
+ this.setEscaper = function(esc){
2200
+ if (typeof esc === 'function'){
2201
+ customEscape = esc;
2202
+ }
2203
+ return this;
2204
+ };
2205
+
2206
+ init.call(this, initOptions);
2207
+ };
2208
+
2209
+
2210
+
2211
+ HopscotchHighlight = function(opt) {
2212
+ this.init(opt);
2213
+ };
2214
+
2215
+ HopscotchHighlight.prototype = {
2216
+ init: function(initOpt) {
2217
+ var opt;
2218
+ var el = {
2219
+ top: document.createElement('div'),
2220
+ left: document.createElement('div'),
2221
+ right: document.createElement('div'),
2222
+ bottom: document.createElement('div')
2223
+ };
2224
+
2225
+ this.element = el;
2226
+
2227
+
2228
+ //Merge highlight options with defaults.
2229
+ opt = {
2230
+ highlight: defaultOpts.highlight,
2231
+ highlightMargin: defaultOpts.highlightMargin
2232
+ };
2233
+
2234
+ initOpt = (typeof initOpt === undefinedStr ? {} : initOpt);
2235
+ utils.extend(opt, initOpt);
2236
+ this.opt = opt;
2237
+
2238
+ for (var e in this.element){
2239
+ utils.addClass(this.element[e], 'hopscotch-overlay');
2240
+ }
2241
+ },
2242
+ addToDom: function(){
2243
+ for (var e in this.element){
2244
+ document.body.appendChild(this.element[e]);
2245
+ }
2246
+ },
2247
+ show: function(){
2248
+ // check if step has disabled the highlight:
2249
+ if (!this.stepOpts.highlight){
2250
+ return;
2251
+ }
2252
+
2253
+ for (var e in this.element){
2254
+ utils.removeClass(this.element[e], 'hide');
2255
+ }
2256
+ },
2257
+ hide: function(){
2258
+ for (var e in this.element){
2259
+ utils.addClass(this.element[e], 'hide');
2260
+ }
2261
+ },
2262
+ setPosition: function(step, targetBounds){
2263
+ // check if step has disabled the highlight:
2264
+ if (!this.stepOpts.highlight){
2265
+ return;
2266
+ }
2267
+
2268
+ var margin = this.stepOpts.highlightMargin;
2269
+
2270
+ var body = document.body,
2271
+ html = document.documentElement;
2272
+
2273
+ var documentHeight = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight );
2274
+
2275
+ // top div:
2276
+ el = this.element.top;
2277
+ el.style.top = '0px';
2278
+ el.style.left = '0px';
2279
+ el.style.width = window.screen.width + 'px';
2280
+ el.style.height = targetBounds.top + utils.getScrollTop() - margin + 'px';
2281
+
2282
+ // right div:
2283
+ el = this.element.right;
2284
+ el.style.top = targetBounds.top + utils.getScrollTop() - margin + 'px';
2285
+ el.style.left = targetBounds.left + targetBounds.width + utils.getScrollLeft() + margin + 'px';
2286
+ el.style.width = window.screen.width - (targetBounds.left + targetBounds.width + utils.getScrollLeft() + margin) + 'px';
2287
+ el.style.height = targetBounds.height + margin * 2 + 'px';
2288
+
2289
+ // bottom div:
2290
+ el = this.element.bottom;
2291
+ el.style.top = targetBounds.top + utils.getScrollTop() + targetBounds.height + margin + 'px';
2292
+ el.style.left = '0px';
2293
+ el.style.width = window.screen.width + 'px';
2294
+ el.style.height = documentHeight - (targetBounds.top + utils.getScrollTop() + targetBounds.height + margin) + 'px';
2295
+
2296
+ // left div:
2297
+ el = this.element.left;
2298
+ el.style.top = targetBounds.top + utils.getScrollTop() + - margin + 'px';
2299
+ el.style.left = '0px';
2300
+ el.style.width = targetBounds.left + utils.getScrollLeft() - margin + 'px';
2301
+ el.style.height = targetBounds.height + margin * 2 + 'px';
2302
+ },
2303
+ render: function(step){
2304
+ // set options for current step:
2305
+ this.stepOpts = {};
2306
+
2307
+ utils.extend(this.stepOpts, this.opt);
2308
+ utils.extend(this.stepOpts, step);
2309
+ }
2310
+ };
2311
+
2312
+
2313
+
2314
+ winHopscotch = new Hopscotch();
2315
+ context[namespace] = winHopscotch;
2316
+
2317
+ // Template includes, placed inside a closure to ensure we don't
2318
+ // end up declaring our shim globally.
2319
+ (function(){
2320
+ var _ = {};
2321
+ /*
2322
+ * Adapted from the Underscore.js framework. Check it out at
2323
+ * https://github.com/jashkenas/underscore
2324
+ */
2325
+ _.escape = function(str){
2326
+ if(customEscape){ return customEscape(str); }
2327
+
2328
+ if(str == null) return '';
2329
+ return ('' + str).replace(new RegExp('[&<>"\']', 'g'), function(match){
2330
+ if(match == '&'){ return '&amp;' }
2331
+ if(match == '<'){ return '&lt;' }
2332
+ if(match == '>'){ return '&gt;' }
2333
+ if(match == '"'){ return '&quot;' }
2334
+ if(match == "'"){ return '&#x27;' }
2335
+ });
2336
+ }
2337
+ this["hopscotch"] = this["hopscotch"] || {};
2338
+ this["hopscotch"]["templates"] = this["hopscotch"]["templates"] || {};
2339
+
2340
+ this["hopscotch"]["templates"]["bubble_default"] = function(obj) {
2341
+ obj || (obj = {});
2342
+ var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
2343
+ function print() { __p += __j.call(arguments, '') }
2344
+ with (obj) {
2345
+
2346
+
2347
+ function optEscape(str, unsafe){
2348
+ if(unsafe){
2349
+ return _.escape(str);
2350
+ }
2351
+ return str;
2352
+ }
2353
+ ;
2354
+ __p += '\n<div class="hopscotch-bubble-container" style="width: ' +
2355
+ ((__t = ( step.width )) == null ? '' : __t) +
2356
+ 'px; padding: ' +
2357
+ ((__t = ( step.padding )) == null ? '' : __t) +
2358
+ 'px;">\n ';
2359
+ if(tour.isTour){ ;
2360
+ __p += '<span class="hopscotch-bubble-number">' +
2361
+ ((__t = ( i18n.stepNum )) == null ? '' : __t) +
2362
+ '</span>';
2363
+ } ;
2364
+ __p += '\n <div class="hopscotch-bubble-content">\n ';
2365
+ if(step.title !== ''){ ;
2366
+ __p += '<h3 class="hopscotch-title">' +
2367
+ ((__t = ( optEscape(step.title, tour.unsafe) )) == null ? '' : __t) +
2368
+ '</h3>';
2369
+ } ;
2370
+ __p += '\n ';
2371
+ if(step.content !== ''){ ;
2372
+ __p += '<div class="hopscotch-content">' +
2373
+ ((__t = ( optEscape(step.content, tour.unsafe) )) == null ? '' : __t) +
2374
+ '</div>';
2375
+ } ;
2376
+ __p += '\n </div>\n <div class="hopscotch-actions">\n ';
2377
+ if(buttons.showPrev){ ;
2378
+ __p += '<button class="hopscotch-nav-button prev hopscotch-prev">' +
2379
+ ((__t = ( i18n.prevBtn )) == null ? '' : __t) +
2380
+ '</button>';
2381
+ } ;
2382
+ __p += '\n ';
2383
+ if(buttons.showCTA){ ;
2384
+ __p += '<button class="hopscotch-nav-button next hopscotch-cta">' +
2385
+ ((__t = ( buttons.ctaLabel )) == null ? '' : __t) +
2386
+ '</button>';
2387
+ } ;
2388
+ __p += '\n ';
2389
+ if(buttons.showNext){ ;
2390
+ __p += '<button class="next hopscotch-next btn btn-primary btn-block btn-sm">' +
2391
+ ((__t = ( i18n.nextBtn )) == null ? '' : __t) +
2392
+ '</button>';
2393
+ } ;
2394
+ __p += '\n </div>\n ';
2395
+ if(buttons.showClose){ ;
2396
+ __p += '<a title="' +
2397
+ ((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
2398
+ '" href="#" class="hopscotch-bubble-close hopscotch-close">' +
2399
+ ((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
2400
+ '</a>';
2401
+ } ;
2402
+ __p += '\n</div>\n<div class="hopscotch-bubble-arrow-container hopscotch-arrow">\n <div class="hopscotch-bubble-arrow-border"></div>\n <div class="hopscotch-bubble-arrow"></div>\n</div>';
2403
+
2404
+ }
2405
+ return __p
2406
+ };
2407
+ }());
2408
+
2409
+ }(window, 'hopscotch'));