-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathscrollmenu.js
480 lines (388 loc) · 19.2 KB
/
scrollmenu.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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
/*
ScrollMenu v 1.0.4
Author: Sudhanshu Yadav
Copyright (c) 2015 to Sudhanshu Yadav - ignitersworld.com , released under the MIT license.
Demo on: http://ignitersworld.com/lab/scrollmenu/
*/
;(function ($, window, document, undefined) {
"use strict";
//a singular method for hquery
$.single = (function () {
var single = $({});
return function (elm) {
single[0] = elm;
return single;
}
}());
//common selectors
var $document = $(document),
$window = $(window),
$body = $('body');
//access global object
var global = (function () {
return this || (1, eval)('this');
}());
//void function
var voidFun = function () {};
//function to get scrollbar width
var scrollBarWidth = (function () {
var outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
var widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
var inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
var widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}());
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
var cache = {};
function tmpl(str, data) {
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"obj = obj || {}; var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',obj.$1 || \"\",'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'") + "');return p.join('');");
// Provide some basic currying to the user
return data ? fn(data) : fn;
};
//default values for plugin
var defaults = {
sectionClass: 'section',
menuType: 'vertical', //can be vertical or horizontal and horizontal-menu
appendTo: null, //defaults to container
animateOnScroll: true,
nativeScroll: true,
scrollbarVisible: false,
onhover: voidFun,
onhoverOut: voidFun,
onscrollToSection: voidFun,
menuMod: voidFun, // a callback to modify menu or add interaction after its been added to menu
anchorSetup: [],
animationDuration: 600,
scrollAnchorSpacing: 10
};
//Scroll menu class;
function ScrollMenu(container, options) {
var self = this; //store this to self, so it can be minified
//check if container not provided take body as default container
if (!container || !($(container)[0] instanceof Element)) {
options = container;
container = document.body;
}
//initilize options
options = options || {};
options = self.options = $.extend({}, defaults, options);
self.container = $(container);
self.sections = self.container.find(self.options.sectionClass);
//set the vertical flag
self._vertical = options.menuType == "vertical";
//arrays to contain information about anchors and sections
self._sectionTops = [];
self._sectionHeights = [];
self._scrollAnchorPos = [];
}
ScrollMenu.prototype = {
constructor: ScrollMenu,
_init: function () {
var self = this,
options = self.options,
nativeScroll = options.nativeScroll,
container = self.container;
//prepare menu html
self._prepareMenu();
var scrollElm = self.scrollElm;
//assign events for watching scroll
if (nativeScroll) {
scrollElm.on('scroll', function () {
self._onScroll();
});
}
//set anchor size and update size on window resize
self._setScrollAchorSize();
$window.on('resize.scrollMenu', function () {
//after a timeout refresh container and scroll menu sizes
if (self.resizeTimeout) window.clearTimeout(self.resizeTimeout);
self.resizeTimeout = setTimeout(function () {
if (nativeScroll) {
//reset the height so css applied height takes over
container.height("");
scrollElm.add(container).height(container.is('body,html') ? $window.height() : container.height());
}
self._setScrollAchorSize();
}, 200);
});
//bind event to controll scroll manually
self.scrollMenuWrap.on('mousedown touchstart', function (e) {
e.preventDefault();
var target = $.single(e.target),
touchMove = e.type == "touchstart" ? "touchmove" : "mousemove",
touchEnd = e.type == "touchstart" ? "touchend" : "mouseup";
if (!target.is('.scroll-handle')) {
var anchorIdx = self.scrollAnchors.index(target.closest('.scroll-anchor'));
if (anchorIdx == -1) return;
setTimeout(function () {
self.scrollToSection(anchorIdx);
}, 0);
} else {
//add dragging class to body so that anywhere user move cursor it should show grabbing cursor
$body.addClass('scroll-menu-dragging');
/** set anchor positions **/
self._setAnchorPos();
var clientAxis = self._vertical ? 'clientY' : 'clientX',
c1 = e[clientAxis] || e.originalEvent.touches[0][clientAxis],
scrollTop = self.scrollTop(),
c1idx = self._inBoundry(c1);
$document.on(touchMove + '.scrollMenu', function (e2) {
e2.preventDefault();
var c2 = e2[clientAxis] || e2.originalEvent.touches[0][clientAxis],
c2idx = self._inBoundry(c2);
if (c2idx !== false) {
self.scrollTo(scrollTop + ((c2 - c1 - (c2idx - c1idx) * options.scrollAnchorSpacing) / self._sizeFactor), false);
}
});
$document.one(touchEnd, function () {
$document.off(touchMove + '.scrollMenu');
//add dragging class to body so that anywhere user move cursor it should show grabbing cursor
$body.removeClass('scroll-menu-dragging');
});
}
});
//add hovered class on menu content on hover of anchor and fire corresponding callbacks
self.scrollAnchors.hover(function (e) {
var $this = $.single(this),
idx = self.scrollAnchors.index(this),
anchorSetup = options.anchorSetup,
onhover = anchorSetup[idx] && anchorSetup[idx].onhover || options.onhover;
$.single(this).find('.scroll-menu-content').addClass('hovered');
onhover.call(this, e, self.getIndexData(idx));
}, function (e) {
var $this = $.single(this),
idx = self.scrollAnchors.index(this),
anchorSetup = options.anchorSetup,
onhoverOut = anchorSetup[idx] && anchorSetup[idx].onhoverOut || options.onhoverOut;
$.single(this).find('.scroll-menu-content').removeClass('hovered');
onhoverOut.call(this, e, self.getIndexData(idx));
});
//trigger scroll method to check the correct scroll position
if (nativeScroll) self._onScroll();
self._setScrollAchorSize();
return self;
},
//method to prepare menu html
_prepareMenu: function () {
var self = this,
container = self.container,
options = self.options,
scrollbarVisible = options.scrollbarVisible;
//wrap container content to hide scrollbars while having native scrolling (A dirty hack :(, looking a better alternative for it)
container.addClass('scroller-container');
if (options.nativeScroll) {
/** scrollbar hide hack start **/
var rightAjust = scrollbarVisible ? 'width:100%' : 'padding-right:10px; right:-' + (10 + scrollBarWidth) + 'px';
container.wrapInner('<div class="content-inner-wrapper"></div>').wrapInner('<div class="content-wrapper" style="' + rightAjust + '"></div>'); //it should be box sizing contentbox
//cache scrollElm for feature use
var scrollElm = self.scrollElm = container.find('.content-wrapper');
//manage height and positions
scrollElm.add(container).height(container.is('body') ? $window.height() : container.height());
scrollElm.css('position', 'absolute');
/** scrollbar hide hack end **/
}
//create scroll anchors
var anchorSetup = options.anchorSetup,
scrollBarHtml = ['<div class="scroll-menu-wrapper ' + self.options.menuType + ' ' + (options.className || "") + '">'];
for (var i = 0, ln = self.sections.length; i < ln; i++) {
var setupObj = anchorSetup[i] || {},
menuTemplate = setupObj.template || options.template || "",
anchorMargin = i == ln - 1 ? '' : 'style="margin-' + (self._vertical ? "bottom" : 'right') + ':' + options.scrollAnchorSpacing + 'px"';
menuTemplate = menuTemplate && '<div class="scroll-menu-content <%= className %>" style="' + (self.options.menuType != "horizontal-menu" && 'background-color : <%= backgroundColor %>') + '">' + menuTemplate + '</div>';
var scrollAnchorTempl = '<div class="scroll-anchor" ' + anchorMargin + '>' + menuTemplate + '<div class="scroll-handle-wrap" style="background-color : <%= backgroundColor %>"><div class="scroll-handle"></div></div></div>';
//compile and push template
scrollBarHtml.push(tmpl(scrollAnchorTempl, setupObj));
}
scrollBarHtml.push("</div>");
//append menu to provided element or container by default
var appendTo = options.appendTo ? $(options.appendTo) : container;
appendTo.append(scrollBarHtml.join(''));
//cache the jquery selectors for future use
var scrollMenuWrap = self.scrollMenuWrap = appendTo.find('.scroll-menu-wrapper');
self.scrollAnchors = scrollMenuWrap.find('.scroll-anchor');
self.scrollHandles = scrollMenuWrap.find('.scroll-handle');
//to apply modification on menu once its added
$.each(self.scrollAnchors, function (idx) {
var menuMod = anchorSetup[idx] && anchorSetup[idx].menuMod || options.menuMod;
menuMod.call(this, self.getIndexData(idx));
});
//add a right padding of scrollbar width on scrollmenu if scrollbar is set to be visible
if (scrollbarVisible) {
var paddingRight = scrollMenuWrap.css('padding-right', (parseInt(scrollMenuWrap.css('padding-right'), 10) + scrollBarWidth) + 'px');
}
},
//function to get scroll position
scrollTop: function () {
return this.scrollElm.scrollTop();
},
//function to get inner content height
_scrollHeight: function () {
var self = this;
return (self.options.nativeScroll ? self.scrollElm : self.container)[0].scrollHeight;
},
//set offset positions of scroll anchor
_setAnchorPos: function () {
var self = this,
winScrollTop = $window.scrollTop(),
winScrollLeft = $window.scrollLeft(),
vertical = self._vertical;
self.scrollAnchors.each(function (idx, elm) {
var $this = $(this),
offset = $this.offset(),
topOffset = offset.top - winScrollTop,
leftOffset = offset.left - winScrollLeft;
self._scrollAnchorPos[idx] = {
min: vertical ? topOffset : leftOffset,
max: vertical ? topOffset + $this.height() : leftOffset + $this.width()
}
});
},
//a function to check if mouse is moving inside anchor
_inBoundry: function (p) {
var scrollAnchorPos = this._scrollAnchorPos,
obj;
for (var i = 0, ln = scrollAnchorPos.length; i < ln; i++) {
obj = scrollAnchorPos[i];
if (p >= obj.min && p <= obj.max) return i;
};
return false;
},
//function to return information of anchor and section at specified index
getIndexData: function (index) {
var self = this;
return {
index: index,
anchor: self.scrollAnchors[index],
section: self.sections[index],
anchorOptions: self.options.anchorSetup[index]
};
},
//hookable scrollTo method
_scrollTo: function (top, duration, callback) {
this.scrollElm.animate({
scrollTop: top + 'px'
}, duration, callback);
},
//function to scroll at defined place
scrollTo: function (top, animate, callback) {
var self = this,
options = self.options;
callback = callback || (typeof animate == "function" ? animate : voidFun);
animate = (animate == undefined || typeof animate == "function") ? true : animate;
var duration = options.animateOnScroll && animate ? options.animationDuration : 0;
self._scrollTo(top, duration, callback);
},
//function to scroll to specified section
scrollToSection: function (section, animate, callback) {
callback = callback || (typeof animate == "function" ? animate : voidFun);
var self = this,
index = !isNaN(section) ? section : self.sections.index(self.sections.filter(section)), //section index
options = self.options,
anchorSetup = options.anchorSetup,
scrollTop = self._sectionTops[index];
if (scrollTop != undefined) {
self.scrollTo(scrollTop, animate, function () {
var indexData = self.getIndexData(index); // data present at calculated index
//call onscroll to section callback
(anchorSetup[index] && anchorSetup[index].onscrollToSection || options.onscrollToSection)(indexData);
//call the provided callback
callback(indexData);
});
}
},
_onScroll: function () {
var self = this;
self._updateScrollAnchors(self.scrollTop());
},
//calculate dimensions for anchors and sections
_setScrollAchorSize: function () {
var self = this,
options = self.options,
container = self.container,
vertical = self._vertical,
scrollMenuWrapper = self.scrollMenuWrap;
var containerHeight = container.is('body,html') ? $window.height() : container.height(),
containerWidth = container.width(),
scrollMenuSize = vertical ? Math.min(containerHeight, scrollMenuWrapper.innerHeight()) : Math.min(containerWidth, scrollMenuWrapper.innerWidth() || 1000000); //height of scrollMenu wrapper in case menu type is vertical and width in case menu type is horizontal
if (scrollMenuSize < 100) {
scrollMenuSize = vertical ? containerHeight : containerWidth;
}
self.scrollHeight = self._scrollHeight();
var menuPaddingStart = parseInt(scrollMenuWrapper.css(vertical ? 'padding-top' : 'padding-left'), 10) || 0,
menuPaddingEnd = parseInt(scrollMenuWrapper.css(vertical ? 'padding-bottom' : 'padding-right'), 10) || 0;
var handleSize = (scrollMenuSize - menuPaddingStart - menuPaddingEnd - options.scrollAnchorSpacing * (self.scrollHandles.length - 1)) * containerHeight / self.scrollHeight;
//set handle size
self.scrollHandles[vertical ? 'height' : 'width'](handleSize);
self._sizeFactor = handleSize / containerHeight;
self.sections.each(function (idx) {
var $this = $.single(this),
secHeight = $this.innerHeight(),
topPos = $this.position().top;
self._sectionHeights[idx] = secHeight;
self._sectionTops[idx] = topPos;
$.single(self.scrollAnchors[idx])[vertical ? 'height' : 'width'](secHeight * handleSize / containerHeight);
});
return self;
},
//update scrollbar inside scroll anchors
_updateScrollAnchors: function (scrollTop) {
var self = this;
self.scrollAnchors.each(function (idx) {
var scrollHandle = $.single(self.scrollHandles[idx]),
perc = (scrollTop - self._sectionTops[idx]) * 100 / self._sectionHeights[idx];
scrollHandle.css(self._vertical ? 'top' : 'left', perc + '%');
});
},
refresh: function () {
this._setScrollAchorSize();
},
destroy: function () {
var self = this;
//remove scrollmenu and wraps
self.scrollMenuWrap.remove();
if (self.scrollElm) {
self.scrollElm.children().children().unwrap().unwrap();
}
//remove scroller container
self.container.removeClass('scroller-container');
//remove event listener
$window.off('resize.scrollMenu');
}
}
//provide a globally accessable api for ScrollMenu
global.ScrollMenu = function (container, options) {
return (new ScrollMenu(container, options))._init();
}
global.ScrollMenu.defaults = defaults;
}(jQuery, window, document));