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.
- data/MIT-LICENSE +20 -20
- data/README.md +62 -61
- data/Rakefile +19 -19
- data/app/assets/javascripts/roroacms/admin/application.js +1 -3
- data/app/assets/javascripts/roroacms/admin/roroacms-tour.js +46 -46
- data/app/assets/javascripts/roroacms/application.js +9 -21
- data/app/assets/javascripts/roroacms/vendor/hopscotch.js +2409 -2409
- data/app/assets/stylesheets/roroacms/admin/application.css +5 -3
- data/app/assets/stylesheets/roroacms/vendor/hopscotch.css +512 -512
- data/app/assets/stylesheets/roroacms/vendor/jquery/jquery-ui.css +1177 -1177
- data/app/controllers/roroacms/admin_controller.rb +1 -0
- data/app/controllers/roroacms/application_controller.rb +5 -3
- data/app/helpers/roroacms/admin_roroa_helper.rb +4 -4
- data/app/helpers/roroacms/admin_view_helper.rb +16 -16
- data/app/helpers/roroacms/application_helper.rb +4 -4
- data/app/helpers/roroacms/general_helper.rb +8 -8
- data/app/helpers/roroacms/new_view_helper.rb +7 -6
- data/app/helpers/roroacms/routing_helper.rb +15 -15
- data/app/helpers/roroacms/seo_helper.rb +2 -1
- data/app/helpers/roroacms/theme_helper.rb +6 -6
- data/app/helpers/roroacms/view_helper.rb +4 -4
- data/app/mailers/roroacms/emailer.rb +31 -31
- data/app/views/layouts/roroacms/application.html.erb +1 -1
- data/app/views/roroacms/admin/partials/_append_sidebar_menu.html.erb +51 -51
- data/app/views/roroacms/admin/partials/_comment.html.erb +10 -1
- data/app/views/roroacms/admin/partials/_editor.html.erb +21 -21
- data/app/views/roroacms/setup/administrator.html.erb +72 -72
- data/app/views/roroacms/setup/index.html.erb +108 -108
- data/config/initializers/devise.rb +258 -258
- data/config/locales/devise.en.yml +115 -115
- data/config/locales/en.yml +1 -1
- data/config/routes.rb +96 -96
- data/db/migrate/20140801203556_add_admins.rb +44 -44
- data/db/migrate/20140802084604_add_roroacms_comments.rb +31 -31
- data/db/migrate/20140802084615_add_roroacms_menu_options.rb +24 -24
- data/db/migrate/20140802084624_add_roroacms_menus.rb +18 -18
- data/db/migrate/20140802084631_add_roroacms_posts.rb +42 -42
- data/db/migrate/20140802084638_add_roroacms_settings.rb +21 -21
- data/db/migrate/20140802084658_add_roroacms_term_anatomies.rb +20 -20
- data/db/migrate/20140802084707_add_roroacms_term_relationships.rb +23 -23
- data/db/migrate/20140802084717_add_roroacms_terms.rb +27 -27
- data/lib/ext/string.rb +4 -4
- data/lib/generators/roroacms/install_generator.rb +16 -16
- data/lib/roroacms/engine.rb +4 -5
- data/lib/roroacms/version.rb +3 -3
- data/lib/roroacms.rb +5 -5
- data/lib/tasks/roroacms_tasks.rake +9 -9
- data/spec/controllers/roroacms/admin/themes_controller_spec.rb +2 -2
- data/spec/dummy/README.rdoc +28 -28
- data/spec/dummy/Rakefile +6 -6
- data/spec/dummy/app/controllers/application_controller.rb +5 -5
- data/spec/dummy/app/helpers/application_helper.rb +2 -2
- data/spec/dummy/app/helpers/theme_helper.rb +12 -12
- data/spec/dummy/app/views/layouts/application.html.erb +14 -14
- data/spec/dummy/app/views/theme/roroa-bootstrap-3/theme_helper.rb +8 -8
- data/spec/dummy/app/views/theme/roroa1/assets/stylesheets/style.css.scss +8 -8
- data/spec/dummy/app/views/theme/roroa1/layout.html.erb +1 -1
- data/spec/dummy/bin/bundle +3 -3
- data/spec/dummy/bin/rails +4 -4
- data/spec/dummy/bin/rake +4 -4
- data/spec/dummy/config/application.rb +23 -23
- data/spec/dummy/config/boot.rb +5 -5
- data/spec/dummy/config/database.yml +7 -7
- data/spec/dummy/config/environment.rb +5 -5
- data/spec/dummy/config/environments/development.rb +37 -37
- data/spec/dummy/config/environments/production.rb +82 -82
- data/spec/dummy/config/environments/test.rb +39 -39
- data/spec/dummy/config/initializers/assets.rb +8 -8
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -7
- data/spec/dummy/config/initializers/cookies_serializer.rb +2 -2
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -4
- data/spec/dummy/config/initializers/inflections.rb +16 -16
- data/spec/dummy/config/initializers/mime_types.rb +4 -4
- data/spec/dummy/config/initializers/session_store.rb +3 -3
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -14
- data/spec/dummy/config/locales/en.yml +23 -23
- data/spec/dummy/config/routes.rb +3 -3
- data/spec/dummy/config/secrets.yml +22 -22
- data/spec/dummy/config.ru +4 -4
- data/spec/dummy/db/schema.rb +149 -149
- data/spec/dummy/public/404.html +67 -67
- data/spec/dummy/public/422.html +67 -67
- data/spec/dummy/public/500.html +66 -66
- data/spec/dummy/public/javascripts/translations.js +1 -1
- data/spec/dummy/tmp/cache/i18n-js.yml +36 -36
- data/spec/models/roroacms/admin_spec.rb +67 -67
- data/spec/spec_helper.rb +35 -35
- data/spec/support/factories.rb +79 -79
- 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., ['一',
|
|
2115
|
-
* '二', '三']) 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., ['一',
|
|
2146
|
-
* '二', '三']) 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 '&' }
|
|
2331
|
-
if(match == '<'){ return '<' }
|
|
2332
|
-
if(match == '>'){ return '>' }
|
|
2333
|
-
if(match == '"'){ return '"' }
|
|
2334
|
-
if(match == "'"){ return ''' }
|
|
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., ['一',
|
|
2115
|
+
* '二', '三']) 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., ['一',
|
|
2146
|
+
* '二', '三']) 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 '&' }
|
|
2331
|
+
if(match == '<'){ return '<' }
|
|
2332
|
+
if(match == '>'){ return '>' }
|
|
2333
|
+
if(match == '"'){ return '"' }
|
|
2334
|
+
if(match == "'"){ return ''' }
|
|
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'));
|