forked from AMKohn/bounceback
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbounceback.js
371 lines (302 loc) · 8.89 KB
/
bounceback.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
/**
* Bounceback.js v1.0.0
*
* Copyright 2014 Avi Kohn
* Distributable under the MIT license
*/
(function(root, factory) {
// The Istanbul comments stop the UMD from being counted in coverage reports
// AMD
/* istanbul ignore next */
if (typeof define === "function" && define.amd) {
define(function() {
return factory(root, document, {});
});
}
// Node.js and CommonJS, for testing
else if (typeof exports !== "undefined") {
// This is a test run, inject the test environment
if (global && global.testEnv) {
factory(global.testEnv, global.testEnv.document, exports);
}
/* istanbul ignore next */
else {
factory(root, document, exports);
}
}
// Normal browser usage
/* istanbul ignore next */
else {
root.Bounceback = factory(root, document, {});
}
// `root` and `doc` allow for better compression
})(window, function(root, doc, Bounceback) {
/**
* Attaches an event to the window.
*
* This could accept an element as an argument but that would make testing more difficult.
*
* @api private
* @param {Element} elm The element to attach the event to
* @param {String} evt The name of the event to attach
* @param {Function} cb The event callback
*/
var addEvent = function(elm, evt, cb) {
if (elm.attachEvent) {
elm.attachEvent("on" + evt, cb);
}
else {
elm.addEventListener(evt, cb, false);
}
};
// There isn't any other library called Bounceback that would use the
// variable, but might as well
var oldBounceback = root.Bounceback;
/**
* Restores the Bounceback variable in the global scope to its previous value
*
* @return {Object} Bounceback
*/
Bounceback.noConflict = function() {
root.Bounceback = oldBounceback;
return this;
};
Bounceback.version = "1.0.0";
Bounceback.options = {
distance: 100, // The minimum distance in px from the top to consider triggering for
maxDisplay: 1, // The maximum number of times the dialog may be shown on one page, or 0 for unlimited. Only applicable when using the mouse based method
method: "auto", // The bounce detection method
sensitivity: 10, // The minimum distance the mouse has to have moved in the last 10 mouse events for onBounce to be triggered
cookieLife: 365, // The cookie (when localStorage isn't available) expiry age, in days
scrollDelay: 500, // The amount of time in ms that bouncing should be ignored for after scrolling, or 0 to disable
aggressive: false, // Whether or not to ignore the cookie that blocks initialization unless it's the first pageview
checkReferrer: true, // Whether or not to check the referring page to see if it's on the same domain and this isn't the first pageview
storeName: "bounceback-visited", // The key to store the cookie (or localStorage item) under
onBounce: function() { return Bounceback; } // The default onBounce handler
};
Bounceback.data = {
/**
* Gets an item's value by key from storage
*
* @api public
* @param {String} key The key to retrieve the value from
* @return {String} The retrieved value
*/
get: function(key) {
if (root.localStorage) {
return root.localStorage.getItem(key) || "";
}
else {
var cookies = doc.cookie.split(";");
var i = -1,
data = [],
cVal = "",
cName = "",
length = cookies.length;
while (++i < length) {
data = cookies[i].split("=");
if (data[0] == key) {
data.shift();
return data.join("=");
}
}
return "";
}
},
/**
* Sets a key to the specified value in storage
*
* @api public
* @param {String} key The key to store under
* @param {String} value The value to store
* @return {Object} The data store, for chained calls
*/
set: function(key, value) {
if (root.localStorage) {
root.localStorage.setItem(key, value);
}
else {
var dt = new Date();
dt.setDate(dt.getDate() + Bounceback.options.cookieLife);
doc.cookie = key + "=" + value + "; expires=" + dt.toUTCString() + ";path=/;";
}
return this;
}
};
var shown = 0;
/**
* This proxies calls to onBounce to ensure that it isn't triggered
* more than the limit specified in the options allows
*/
Bounceback.onBounce = function() {
shown++;
/* istanbul ignore else */
if (!this.options.maxDisplay || shown <= this.options.maxDisplay) {
this.options.onBounce();
}
};
/**
* Whether or not the current browser is mobile.
*
* This is used to decide if the mouse or pushState method should be used.
*
* @type {Boolean}
*/
Bounceback.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(root.navigator.userAgent);
/**
* Whether or not Bounceback is disabled, toggled by default on scroll
*
* @type {Boolean}
*/
Bounceback.disabled = false;
/**
* Whether or not Bounceback has been activated. This prevents activate
* from executing more than once
*
* @type {Boolean}
*/
Bounceback.activated = false;
/**
* Disables Bounceback.
*
* This does _not_ remove the event handlers since that would involve
* more complicated code to handle each of the handlers when only one
* would ever be attached at a given time.
*
* @api public
* @return {Object} Bounceback
*/
Bounceback.disable = function() {
this.disabled = true;
return this;
};
/**
* Enables Bounceback
*
* @api public
* @return {Object} Bounceback
*/
Bounceback.enable = function() {
this.disabled = false;
return this;
};
/**
* Attaches handlers as necessary and sets up Bounceback
*/
Bounceback.activate = function(method) {
if (method == "history") {
// The history API for modern browsers
if ("replaceState" in root.history) {
// Set data in the current state to let Bounceback know that it should
// fire the onBounce handler
root.history.replaceState({
isBouncing: true
}, root.title);
// Then add a new state to the history so hitting back navigates to
// the previous added state and fires onBounce
root.history.pushState(null, root.title);
// Handle popstate events
addEvent(root, "popstate", function(e) {
/* istanbul ignore else */
if (root.history.state && root.history.state.isBouncing) {
Bounceback.onBounce();
}
});
}
// And the hash for others
/* istanbul ignore else */
else if ("onhashchange" in root) {
// BHT -> Bounceback Hash Trigger
root.location.replace("#bht");
root.location.hash = "";
addEvent(root, "hashchange", function() {
/* istanbul ignore else */
if (root.location.hash.substr(-3) === "bht") {
Bounceback.onBounce();
}
});
}
}
else {
var timer = null,
movements = [],
scrolling = false;
addEvent(doc, "mousemove", function(e) {
movements.unshift({
x: e.clientX,
y: e.clientY
});
movements = movements.slice(0, 10);
});
addEvent(doc, "mouseout", function(e) {
/* istanbul ignore else */
if (!Bounceback.disabled) {
var from = e.relatedTarget || e.toElement;
/* istanbul ignore else */
if (
(!from || from.nodeName == "HTML") &&
e.clientY <= Bounceback.options.distance &&
movements.length == 10 &&
movements[0].y < movements[9].y &&
movements[9].y - movements[0].y > Bounceback.options.sensitivity
) {
Bounceback.onBounce();
}
}
});
// While scrolling using the mouse if it leaves the body the mouseout event is
// delayed until scrolling stops. This ensures that the event fired then is ignored.
/* istanbul ignore else */
if (this.options.scrollDelay) {
addEvent(root, "scroll", function(e) {
/* istanbul ignore else */
if (!Bounceback.disabled) {
Bounceback.disabled = true;
clearTimeout(timer);
timer = setTimeout(function() {
Bounceback.disabled = false;
}, Bounceback.options.scrollDelay);
}
});
}
}
};
/**
* Initializes Bounceback.
*
* Multiple calls will update options that are not already in use.
*
* @api public
* @param {Object} [options] Any options to initialize with
* @return {Object} Bounceback
*/
Bounceback.init = function(options) {
options = options || {};
var key;
for (key in this.options) {
if (this.options.hasOwnProperty(key) && !options.hasOwnProperty(key)) {
options[key] = this.options[key];
}
}
this.options = options;
if (options.checkReferrer && doc.referrer) {
var a = doc.createElement("a");
a.href = doc.referrer;
if (a.host == root.location.host) {
this.data.set(options.storeName, "1");
}
}
if (!this.activated && (options.aggressive || !this.data.get(options.storeName))) {
this.activated = true;
if (options.method === "history" || (options.method === "auto" && this.isMobile)) {
this.activate("history");
}
else {
this.activate("mouse");
}
this.data.set(options.storeName, "1");
}
return this;
};
return Bounceback;
});